-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Architectural boundaries, deployable components, microservices
- Loading branch information
1 parent
d51dfb7
commit b61d1a6
Showing
23 changed files
with
599 additions
and
10 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
# Deployable components | ||
|
||
See: | ||
|
||
- Clean Architecture (book by Robert C. Martin) | ||
- [Clean Architecture – Components and Component Cohesion](https://www.codingblocks.net/podcast/clean-architecture-components-and-component-cohesion/) | ||
|
||
## Basic idea | ||
|
||
Deployable components: | ||
|
||
- Smallest parts that can be deployed separately | ||
- Think JAR files, DLL files, ... | ||
- Ways of deployment | ||
- Could get linked into single executable | ||
- Could be aggregated into single archive (example: .war file) | ||
- Could be independently deployed as some kind of dynamically loaded plugins | ||
- Regardless of how they end up being deployed, in principle they are independently deployable and thus independently developable | ||
- Example: Executable is built from component A, B and C where both A and B depend on C. Because they are indepedently deployable components, new versions of C might be developed while still including an older version of C in the executables until A and B are adjusted to those new versions. | ||
|
||
## Component cohesion | ||
|
||
Answer to the question "which classes belong in which components?" | ||
|
||
Three important principles: | ||
|
||
- **REP:** Reuse-Release Equivalence Principle | ||
- "The granule of reuse is the granule of release" | ||
- Classes and modules within a release should be releasable together | ||
- There must be some overarching theme or purpose that everything within a release shares | ||
- The fact that classes and modules in a release share the same version numbering and same release documentation should make sense to both the author and the users | ||
- Releases of shareable software components/modules must produce the proper notifications and release documentation | ||
- Release numbers are important for measuring compatibility and for communicating changes | ||
- See also [Semantic Versioning](https://semver.org/) | ||
- breaking.new-features.fixes | ||
- This principle is inclusive: tends to make components larger | ||
- **CCP**: Common Closure Principle | ||
- "Components should consist of classes that change for the same reason and at the same times" | ||
- Similar to Single Responsibility Principle in [SOLID principles](oo-design/SOLID-principles.md) | ||
- If the code of an application must change, you would ideally have all the changes to occur in a single component, so you only have to redeploy that single component | ||
- Other components depending on that single component do not need to be revalidated or redeployed | ||
- Relation to Open-Closed Principle from [SOLID principles](oo-design/SOLID-principles.md): group together the classes that are closed to the same type of changes | ||
- This principle is inclusive: tends to make components larger | ||
- **CRP**: Common Reuse Principle | ||
- "Don’t force users of a component to depend on things that they don’t need" | ||
- Similar to Interface Segregation Principle from [SOLID principles](oo-design/SOLID-principles.md) | ||
- If your component has a dependency on another component, any time that depended upon component is changed, your component will need to be revalidated/recompiled | ||
- This is true even if you don't care about the changes in that depended upon component | ||
- Try to make sure that the classes in a component are inseparable, that it is almost impossible to depend on some but not on the others | ||
- This principle is exclusive: tends to make components smaller | ||
|
||
These three tend to pull the answer to "which classes belong in which components?" in different directions | ||
|
||
Tension diagram (edges of diagram show the cost of abandoning the principle on the opposite vertex): | ||
|
||
![Tension diagram](_img/Deployable-components/cohesion-principles-tension-diagram.jpg) | ||
|
||
- Are “too many” components changing at the same time? Then your classes should be consolidated better (CCP) | ||
- Do you have “too many” releases? You should take a look at streamlining and minimizing your dependencies (CRP) | ||
- Is your code hard to re-use? You should improve your release artifacts (release numbers, documentation, notifications) (REP) | ||
|
||
> “A good architect finds a position in that tension triangle that meets the *current* concerns of the development team, but is also aware that those concerns will change over time.” – Robert C. Martin | ||
## Component coupling | ||
|
||
This is about the coupling that is caused by dependencies between components | ||
|
||
### Acyclic Dependencies Principle | ||
|
||
"When you draw a diagram of your components, using arrows to indicate dependencies, you should not have any cycles in that graph - the graph should be a Directed Acyclic Graph (DAG)" | ||
|
||
If the graph is indeed a DAG, it is easy to find out which components could be affected by a change in a certain component: just follow the dependency arrows backwards. Additionally, you can easily find a "topological ordering" for the graph which gives you the order in which to build the components so every component's dependencies are built before the component itself needs to be built. | ||
|
||
If you have a cycle, then the components in that cycle are not really independently developable and deployable! | ||
|
||
- People working on these components must make sure to all use exactly the same release of each other's components | ||
- If A -> B -> C -> A, a new version of A must be compatible with a new version of C which is compatible with that new version of A | ||
- Very difficult to test your component in isolation because your dependencies also depend on the changes you make | ||
- Essentially, components in the cycle are reduced to a single component | ||
|
||
#### Breaking cycles | ||
|
||
Example cycle: A -> B -> C -> A | ||
|
||
Two primary mechanisms: | ||
|
||
- Applying the Dependency Inversion Principle from [SOLID principles](oo-design/SOLID-principles.md) to invert the direction of dependencies | ||
- Example: Reverse dependency between A and C so we now have C <- A -> B -> C | ||
- Creating a new component that two components can depend on instead of one depending on the other | ||
- Example: Create new component D that A and C depend on so we now have D < - A -> B -> C -> D | ||
|
||
### Stability and volatility | ||
|
||
- Some components will be *volatile*: intended and expected to change | ||
- Example: user interface | ||
- Other components will be *stable*: intended and expected not to change (or not often) | ||
- Example: core business logic | ||
|
||
Stability is closely related to the structure of dependencies! | ||
|
||
- If a component does not depend on any other components but several other components depend on it, it is likely to be stable | ||
- No dependencies on others: not forced to change by others | ||
- Others depending on the it: changes to the component require a lot of work because others likely need to be adjusted accordingly | ||
- Special case common in statically typed languages: abstract components that contain nothing but some interfaces, don't depend on anything but are highly depended upon by others | ||
- If a component depends on several other components but no other components depend on it, it is likely to be volatile | ||
- Depends on several others: each of those others can force it to change | ||
- No others depending on it: free to change without affecting others | ||
|
||
Possible metric for stability: # outgoing dependencies / (# incoming dependencies + # outgoing dependencies). Value is 0 if maximally stable, 1 if maximally volatile. | ||
|
||
#### Stable Dependencies Principle | ||
|
||
"You should depend on more stable components, not on more volatile ones" | ||
|
||
- If stable component depends on a volatile one, this means that the volatile one is expected to change often, which will force the stable component to become volatile as well | ||
- If you design a component to be easy to change (volatile), all your efforts to make change easy become undone once someone starts depending on your component | ||
|
||
#### Stable Abstractions Principle | ||
|
||
"More stable components should also be more abstract components" | ||
|
||
Here, "abstractness" could be defined as the ratio of the number of classes in the component versus the number of those classes that are abstract classes or interfaces. Value = 0 if all concrete, 1 if all abstract. | ||
|
||
Reasoning: | ||
|
||
- The problem: It makes sense to have core business logic in a stable component that several others depend upon. However, the fact that it's hard to change could make overall architecture inflexible. How do we make a stable component flexible enough to change? | ||
- The solution: use the Open-Closed Principle (from [SOLID principles](oo-design/SOLID-principles.md)) to create classes that are flexible enough to be extended without needing modification | ||
- Originally aimed at inheritance, making classes easily extensible, which can often lead to making them abstract or even just defining an interface instead of a class | ||
- Note: inheritance is not the only way to make a class extensible! We could also make the class support different kinds of behavior by passing in different objects (dependency injection, Strategy pattern). However, in that case, the objects to pass in will likely be specified by interfaces defined inside the component, again making the component more abstract. | ||
|
||
## Evolving the component structure | ||
|
||
Component structure doesn't need to reflect the different functional areas of the system! Instead, it reflects the buildability and maintainability of the application. | ||
|
||
Likely, top-down design of component structure will not work well! | ||
|
||
What typically works: | ||
|
||
- As the application grows, identify parts of the codebase where putting them in separate components would make development and maintenance easier | ||
- Want to keep changes as localized as possible, so start paying attention to CCP | ||
- As application continues to grow, more concern over creating reusable elements -> CRP | ||
- As cycles appear, apply ADP, changing dependency directions or creating new components |
Binary file added
BIN
+14.5 KB
architecture-design/_img/Architectural-boundaries/clean-architecture-boundary.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.67 KB
architecture-design/_img/Architectural-boundaries/strategy-pattern.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+47.3 KB
...cture-design/_img/Deployable-components/cohesion-principles-tension-diagram.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
113 changes: 113 additions & 0 deletions
113
architecture-design/reference-architectures/Microservices.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# Microservices | ||
|
||
See: | ||
|
||
- Building Evolutionary Architectures (book by Neal Ford, Rebecca Parsons and Patrick Kua) ([summary slides](https://www.slideshare.net/thekua/building-evolutionary-architectures)) | ||
- Clean Architecture (book by Robert C. Martin) | ||
- [BoundedContext](https://www.martinfowler.com/bliki/BoundedContext.html) | ||
- [Pattern: Database per service](https://microservices.io/patterns/data/database-per-service.html) | ||
- [How to keep relationship integrity with Microservice Architecture](https://softwareengineering.stackexchange.com/questions/381279/how-to-keep-relationship-integrity-with-microservice-architecture) | ||
|
||
## Basic idea | ||
|
||
![Microservices](_img/Microservices/microservices.png) | ||
|
||
- The system is divided into several small services that each encapsulate a certain functional area across several layers of the technical stack, even down to the database | ||
- "Shared nothing", decrease coupling between services as much as possible | ||
- Sharing of a database between services is generally considered bad practice, because it prevents services from independently making changes to their database structure (or independently choosing the database technology which makes the most sense for the service) | ||
- Freedom to choose or change the technology used by a service based on what makes most sense. Some service may use a relational database while another one uses a document store. A service providing information about the relationships between different users could switch to a graph database without any other service being affected by the change. | ||
|
||
- Different small teams each take ownership of one or more of these services | ||
- Team responsible for a microservice takes control of development and deployment | ||
- Each service is expected to handle reasonable error scenarios and recover if possible | ||
- Changes within a single functional domain (= single service) can happen within a single team | ||
- Coordination with other teams is only required if the communication with their services needs changes as well | ||
- Services integrate with each other by passing messages, most often over HTTP or message queues | ||
- Typically combined with Continuous Delivery, automatic machine provisioning and deployment, ... | ||
- Monitoring and logging typically first-class architectural concepts | ||
- Several services, each of which might be scaled across several instances -> large number of processes to watch | ||
- This means having a good monitoring and logging setup is essential in keeping the system running smoothly | ||
|
||
## Coupling in a microservices architecture | ||
|
||
The goal of a microservices architecture is typically to minimize inappropriate coupling. However, some coupling is still needed to create a useful system. | ||
|
||
### Integration coupling | ||
|
||
This type of coupling is the most obvious one: The fact that services communicate with each other means that services will still depend on each other to some extent. If your service needs customer data, there are scenarios where a change to the Customer service could impact you. As always, these dependencies should be carefully managed. | ||
|
||
### Service template coupling | ||
|
||
*Service template*: shared template that all services can build upon | ||
|
||
- Contains general stuff like monitoring, logging, and authentication/authorization. | ||
|
||
- Teams maintaining the services build their services upon this template | ||
- Much easier to ensure compliance and manage upgrades to monitoring system etc. than if each service team would build their own version of this | ||
- When the infrastructure team pushes an upgrade to the service template, the services pick it up the next time they go through the deployment pipeline | ||
|
||
Example general service templates: [DropWizard]([https://www.dropwizard.io](https://www.dropwizard.io/)) and [Spring Boot](https://spring.io/projects/spring-boot) | ||
|
||
## Data duplication and bounded contexts | ||
|
||
What if different services are interested in the same concept? Remember, sharing databases between services is considered bad practice! | ||
|
||
Typical approach: each service stores its own representation of the concept | ||
|
||
This can be seen as duplication, but: | ||
|
||
- If a service stores the data it needs, it is less dependent on other services (both regarding availability and code coupling) | ||
- Data that different services store for that same thing will depend on what the specific services need | ||
- Same domain concept may have completely different representations in different services | ||
- Data (or shape of data) regarding the same concept in different services will likely change at different times and for different reasons | ||
|
||
This approach corresponds to the DDD concept of [Bounded Contexts](https://www.martinfowler.com/bliki/BoundedContext.html) | ||
|
||
Example: e-commerce system using a microservices architecture | ||
|
||
- Situation: | ||
- Customer service which manages a whole lot of data about each customer, including their shipping address | ||
- Order service, which maintains orders and needs the shipping address of the customer | ||
- Approach 1: let Order service store a customer ID with each Order | ||
- Whenever all information for an order needs to be retrieved, customer’s address is retrieved from the Customer service | ||
- Problem: if the Customer service is unavailable or the customer has been deleted, the Order service has no idea which address to use for the order | ||
- Problem: if the customer’s address changes, the current shipping address for a customer may not be the address that a certain order was shipped to | ||
- Approach 2: let Order service store customer ID and customer’s shipping address | ||
- Essentially, Order service stores a snapshot of the data it needs | ||
- If the order is retrieved from the Order service later on, the service will use the shipping address that it has stored | ||
|
||
Some services may even be entirely focused on aggregating bits of data from other services! | ||
|
||
- Can be useful for search, analysis, ... | ||
|
||
- Example: functionality to view some general info about the most recent orders of customers, allowing to filter customers by some key attributes | ||
- Can make sense to put this in separate Insights service | ||
- Service gathers data from the Customer and Order services (could be push, through API calls or message queue, or pull) | ||
- Service stores data in a format that includes exactly the required information | ||
- When the service receives a request, it can gather the required data directly from its own database | ||
|
||
### Micro frontends | ||
|
||
See: | ||
|
||
- [Micro Frontends](https://micro-frontends.org/) | ||
- [Micro frontends—a microservice approach to front-end web development](https://medium.com/@tomsoderlund/micro-frontends-a-microservice-approach-to-front-end-web-development-f325ebdadc16) | ||
- [MicroFrontends](https://martinfowler.com/articles/micro-frontends.html) | ||
|
||
Even with microservices in the backend, the frontend is often monolithic: | ||
|
||
![Monolithic frontend](_img/Microservices/monolithic-frontend.png) | ||
|
||
- Often a single, large and feature-rich single-page app cutting across all of the functional areas represented by the backend microservices | ||
- Problems: | ||
- Frontend can become so big that it’s difficult to maintain | ||
- Frontend often also developed by a separate team, meaning that that team needs to coordinate with the backend teams when building functionality. | ||
|
||
Alternative approach: Micro frontends | ||
|
||
![Monolithic frontend](_img/Microservices/micro-frontend.png) | ||
|
||
- Frontend is split into different parts in the same way that the backend is | ||
- Teams are responsible for their functional part of the application across the entire stack, from the database up to the frontend. | ||
- The actual frontend that the user interacts with is stitched together from the functional parts developed by different teams | ||
- Some patterns/frameworks available that can help |
Binary file added
BIN
+36.8 KB
architecture-design/reference-architectures/_img/Microservices/micro-frontend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+285 KB
architecture-design/reference-architectures/_img/Microservices/microservices.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+44.5 KB
...cture-design/reference-architectures/_img/Microservices/monolithic-frontend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.