Skip to content

Commit adb6d98

Browse files
committed
Add other ADRs
1 parent c680815 commit adb6d98

12 files changed

+466
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 5. Create one REST API module
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-04
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
We need to expose the API of our application to the outside world. For now, we expect one client of our application - FrontEnd SPA application.
14+
15+
## Possible solutions
16+
1. Create one .NET Core MVC host application which contains all endpoints. This host application will have references to all business modules and communicates with them directly:</br>
17+
18+
Host/API references:</br>
19+
Administration module</br>
20+
Meetings module</br>
21+
Payments module</br>
22+
User Access module</br>
23+
24+
2. Create one .NET Core MVC host application and multiple APIs projects per module. Each API project should have endpoints which are handled by particular business module:</br>
25+
26+
Host references:</br>
27+
Administration API references Administration module</br>
28+
Meetings API references Meetings module</br>
29+
Payments API references Payments module</br>
30+
User Access API references User Access module</br>
31+
32+
## Decision
33+
34+
Solution 1.
35+
36+
Creating separate API projects for each module will add complexity and little value. Grouping endpoints for a particular business module in a special directory is enough. Another layer on top of the module is unnecessary.
37+
38+
## Consequences
39+
- We will have only one API layer/module
40+
- Each controller has responsibility to delegate Command/Query processing to appropriate module
41+
- We don't need to scan other projects than host for controllers, routes and other MVC mechanisms
42+
- API configuration is easier
43+
- Overall complexity of API layer is lower
44+
- Complexity of each controller is a little bit higher
45+
- Build time will be shorter (less projects)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# 6. Create façade between API and business module
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-04
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
Our API layer should communicate with business modules to fulfill client requests. To support the maximum level of autonomy, each module should expose a minimal set of operations (the module API/contract/interface).
14+
15+
## Decision
16+
17+
Each module will provide implementation for one interface with 3 methods:</br>
18+
19+
```csharp
20+
Task<TResult> ExecuteCommandAsync<TResult>(ICommand<TResult> command);
21+
22+
Task ExecuteCommandAsync(ICommand command);
23+
24+
Task<TResult> ExecuteQueryAsync<TResult>(IQuery<TResult> query);
25+
```
26+
27+
This interface will act as a façade (Façade pattern) between API and module. Only Commands, Queries and returned objects (which are part of this interface) should be visible to the API. Everything else should be hidden behind the façade (module encapsulation).
28+
29+
## Consequences
30+
- API can communicate with the module only by façade (the interface).
31+
- Implementation of API is simpler
32+
- We can change module implementation and API does not require change if the interface is not changed
33+
- We need to focus on module encapsulation, sometimes it involves additional work (like instantiation using internal constructors)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# 7. Use CQRS architectural style
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-04
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
Our application should handle 2 types of requests - reading and writing. </br>
14+
15+
For now, it looks like:</br>
16+
17+
- for reading, we need data model in relational form to return data in tabular/flattened way (tables, lists, dictionaries).
18+
- for writing, we need to have a graph of objects to perform more sophisticated work like validations, business rules checks, calculations.
19+
20+
## Decision
21+
22+
We applied the CQRS architectural style/pattern for each business module. Each module will have a separate model for reading and writing. For now, it will be the simplest CQRS implementation when the read model is immediate consistent. This kind of separation is useful even in simple modules like User Access.
23+
24+
## Consequences
25+
- Façade method of each module should take as parameter only Command or Query object
26+
- We have optimized models for writes and reads (SRP principle).
27+
- We can process Commands and Queries in different ways
28+
- As Command or Query is an object, we can easily serialize them and save/log them.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# 8. Allow return result after command processing
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-04
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
The theory of the CQRS and the CQS principle says that we should not return any information as the result of Command processing. The result should be always "void". However, sometimes we need to return some data immediately as part of the same request.
14+
15+
## Decision
16+
17+
We decided to allow in some cases return results after command processing. Especially, when we create something and we need to return the ID of created object or don't know if request is Command or Query (like Authentication).
18+
19+
## Consequences
20+
- We will have two definitions of Commands and CommandHandlers - with and without result
21+
- It will add some complexity to processing commands (like implementation of decorators)
22+
- We can immediately return the ID of created object/resource. We don't need a second call (query) to retrieve this ID.
23+
- We should be careful to not overuse this approach (sticking as much as possible to the CQRS)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# 9. Use 2 layered architectural style for reads
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-04
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
We applied the CQRS style (see [ADR 7. Use CQRS architectural style](007-use-cqrs-architectural-style.md)), now we need to decide how to handle reading (querying) requests.
14+
15+
## Decision
16+
17+
We will use 2 layered architecture to handle queries: API layer and Application Service layer. As we applied the CQRS and created a separated read model, querying should be straightforward so 2 layers are enough. The API layer is responsible for Query creation based on HTTP request and the module Application layer is responsible for query handling.
18+
19+
## Consequences
20+
- Whole query handling logic is in Application Service layer
21+
- Application Service layer is coupled to the database and querying framework
22+
- Solution is simple, easy to understand
23+
- We don't abstract over database
24+
- Performance is better (no object mapping between layers, querying database almost immediately)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# 10. Use Clean Architecture for writes
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-05
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
We applied the CQRS style (see [ADR 7. Use CQRS architectural style](007-use-cqrs-architectural-style.md)), now we need to decide how to handle writing operations (Commands).
14+
15+
## Decision
16+
17+
We will use **Clean Architecture** to handle commands with 4 layers: **API layer**, **Application Service layer** **Infrastructure layer** and **Domain layer**. </br>
18+
We need to add Domain layer because domain logic will be complex and we want to isolate this logic from other stuff like infrastructure or API. Isolation of domain logic supports testing, maintainability and readability.
19+
20+
## Consequences
21+
- Complexity of the whole solution is higher - we need to add two more layers - domain and infrastructure
22+
- Complexity of implementation of business logic will be smaller - we can focus only on business concerns on this layer
23+
- Complexity of implementation of infrastructure will be smaller - we can focus only on infrastructure concerns on this layer
24+
- Business logic will be testable (no references to other layers)
25+
- Business logic will be independent of persistence (to some level)
26+
- In the Domain layer, we will have the same level of abstraction (close to business)
27+
- Application layer will have louse coupling to the Domain layer and Infrastructure layer (depend on abstractions only)
28+
- We use one of the most popular application architecture - developers are familiar with it
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 11. Create rich Domain Models
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-05
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
We need to create Domain Models for all of the modules. Each Domain Model should represent a solution that solves a particular set of Domain problems (implements business logic).
14+
15+
## Possible solutions
16+
1. Create Anemic Domain Model (Data Model) and implement *Transaction Script* pattern together with *Active Record* pattern
17+
2. Put all business logic to database in stored procedures
18+
3. Create a Rich Domain Model
19+
20+
## Decision
21+
22+
Solution number 3 - Rich Domain Model</br>
23+
24+
1 - no, because the procedural style of coding will not be enough. We want to focus on behavior, not on the data.</br>
25+
2 - no, keeping business logic in the database is not a good idea in that case, object-oriented programming is better than T-SQL to model our domain and we don't have performance architectural drivers to resign from OOD.</br>
26+
27+
We expect complex business logic with different rules, calculations and processing so we want to get as much as possible from Object-Oriented Design principles like abstraction, encapsulation, polymorphism. We want to mutate the state of our objects only through methods (abstraction) to encapsulate all logic and hide implementation details from the client (the Application Service Layer and Unit Tests).</br>
28+
29+
## Consequences
30+
- All objects should be encapsulated (private by default principle)
31+
- Encapsulation of objects implies more work in infrastructure (mapping to private fields, collections is harder)
32+
- Encapsulation of objects decreases to some level testability of these objects (Object-Oriented Design vs Testable Design)
33+
- All public methods of domain objects create Domain Model API
34+
- Implementation details of business logic are hidden
35+
- Clients of Domain Model are easier to implement
36+
- Better object-oriented programming skills are required to implement Rich Domain Model
37+
- Is easier to protect business rules/invariants using Rich Domain Model
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 12. Use Domain-Driven Design tactical patterns
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-05
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
We decided to use the Clean Architecture ([ADR #10](0010-use-clean-architecture-for-writes.md)) and create Rich Domain Models ([ADR #11](0011-create-rich-domain-models.md)) for each module. We need to define or use some construction elements / building blocks to implement our architecture and business logic.
14+
15+
## Decision
16+
17+
We decided to use **Domain-Driven Design** tactical patterns. They focus on the Domain Model implementation. Especially we will use the following building blocks:
18+
19+
- Command - public method on Aggregate (behavior)
20+
- Domain Event - the immutable class which represents important fact occurred on a special point of time (behavior)
21+
- Entity - class with identity (identity cannot change) with mutable attributes which represents concept from domain
22+
- Value Object - immutable class without an identity which represents concept from domain
23+
- Aggregate - cluster of domain objects (Entities, Value Objects) with one class entry point (Entity as Aggregate Root) which defines the boundary of transaction/consistency and protects business rules and invariants
24+
- Repository - collection-like abstraction to persist and load particular Aggregate
25+
- Domain Service - stateless service to execute some business logic which does not belong to any of Entity/Value Object
26+
27+
## Consequences
28+
- We need to define entities and value objects
29+
- We need to define aggregates boundaries
30+
- We need to add repositories for each aggregate
31+
- We can invoke only public methods on Aggregate Roots, everything else should be hidden
32+
- Developers need to be familiar with DDD tactical patterns
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 13. Protect business invariants using exceptions
2+
3+
Date: 2019-07-01
4+
5+
Log date: 2019-11-05
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
Aggregates should check business invariants. When the invariant is broken, we should stop processing and return an error immediately to the client.
14+
15+
## Possible solutions
16+
17+
### 1. Use exceptions
18+
#### Pros
19+
- we can stop processing immediately (fail-fast)
20+
- popular approach in C#
21+
- we don't need to check the result of each method (if-else statements)
22+
- we can catch all Business Exceptions in one place and translate them (for example in the API layer to some HTTP response code).
23+
#### Cons
24+
- indirection
25+
- little performance impact
26+
- for special cases, we need to add a specific catch.
27+
### 2. Return Result object
28+
#### Pros
29+
- no indirection
30+
- no performance impact
31+
- signature method is more descriptive.
32+
### Cons
33+
- we need to add checks result of each method (if-else statements)
34+
- approach is less-known in the C# world
35+
- it needs a library or more coding to support Results
36+
37+
## Decision
38+
39+
Solution number 1 - Use exceptions. </br>
40+
41+
Performance cost of throwing an exception is irrelevant, we don't want too many if/else statements in entities, more familiar with exceptions approach.
42+
43+
## Consequences
44+
- We need to add special *BusinessException* class to separate business rules validation exceptions from other exceptions
45+
- We need to create different business exceptions for each business rule
46+
- We will have a small performance impact (throwing exceptions)
47+
- We will have generic mechanism which catches *BusinessException*
48+
- We will not have a lot of if/else statements in Entities/Value Objects to check method results
49+
- Some monitoring tools logs automatically each exception. If we want to use one of this tool we should be aware of this and figure it out proper solution
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# 14. Event-driven communication between modules
2+
3+
Date: 2019-07-15
4+
5+
Log date: 2019-11-09
6+
7+
## Status
8+
9+
Accepted
10+
11+
## Context
12+
13+
Each module should be autonomous. However, communication between them must take place. We have to decide what will be the preferred way of communication and integration between modules.
14+
15+
## Possible solutions
16+
17+
### 1. Direct method call (synchronous)
18+
19+
Each Module will expose a set of methods (interface, module API) which can be called by other modules directly.
20+
21+
#### Pros
22+
- easier implementation
23+
- no indirection
24+
- more natural in the monolith architecture
25+
- supports immediate consistency
26+
#### Cons
27+
- less autonomy
28+
- strong coupling between modules
29+
- direct method call is blocking
30+
- module has a dependency on another module
31+
32+
### 2. Event-driven (asynchronous)
33+
34+
Each module will publish a specific set of events. Other modules can subscribe to specific events. It is the implementation of _Publish/Subscribe_ pattern.
35+
36+
#### Pros
37+
- more autonomy
38+
- coupling is only to middleware/broker of events
39+
- no blocking communication
40+
- stronger modules boundaries
41+
- module does not have a dependency on another module
42+
#### Cons
43+
- indirection
44+
- more complex solution
45+
- middleware/broker is needed
46+
- does not support immediate consistency
47+
48+
## Decision
49+
50+
Solution number 2 - Event-driven (asynchronous)</br>
51+
52+
We want to achieve the maximum level of autonomy and loose coupling between modules. Moreover, we don't want dependencies between modules. We allow direct calls in the future, but this should be an exception, not a rule.
53+
54+
## Consequences
55+
- We need to implement the Publish/Subscribe pattern
56+
- Solution will be more complex
57+
- Modules will have more autonomy
58+
- Modules will have coupling to broker/middleware
59+
- During modules integration, eventual consistency will occur (asynchronous communication)
60+
- Events become Published Language of our Bounded Contexts (modules)
61+
- Events structure should be stable as much as possible

0 commit comments

Comments
 (0)