-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
446 additions
and
10 deletions.
There are no files selected for viewing
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,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 Ermlab sp. z o. o. | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
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,69 @@ | ||
## Seedwork | ||
|
||
|
||
## Architecture of Catalog module (2021-05-25) | ||
|
||
What kind of architecture should we choose for the *Catalog* module? Should we use CQRS or maybe something simpler and easier to implement? Let's see what others says about it? | ||
|
||
> CQRS stands for Command Query Responsibility Segregation. It's a pattern that I first heard described by Greg Young. At its heart is the notion that you can use a different model to update information than the model you use to read information. For some situations, this separation can be valuable, but beware that for most systems CQRS adds risky complexity. [1] | ||
Since *Catalog* module is mostly about managing list items, it will likely contain a lot of CRUD-like functionality. We don't need separate read and writes models in this case, it seems like an overkill to me. So having full CQRS is not worth it. However, sticking to separation of commands and queries still looks tempting. Let's check out the alternatives: | ||
|
||
1. "The typical entry point for this in DDD is an Application Service. Application services orchestrate calls to repositories and domain objects" [2][3]. | ||
|
||
|
||
Maybe we could implementing using a generic CRUD handler? | ||
|
||
|
||
## Handling commands | ||
|
||
The typical way of reading the system state via queries and changing the system state is via comands. The high-level route controller code could look like: | ||
|
||
``` | ||
def get_route_controller(request, module): | ||
module.execute_query(MyQuery( | ||
foo=request.GET.foo, | ||
bar=request.GET.bar, | ||
)) | ||
return Response(HTTP_200_OK) | ||
def post_route_controller(request, module): | ||
result = module.execute_command(MyCommand( | ||
foo=request.POST.foo, | ||
bar=request.POST.bar, | ||
)) | ||
return Response(HTTP_200_OK) | ||
``` | ||
|
||
In this case `execute_command` is responsible for passing a command to an appriopriate command handler. | ||
|
||
|
||
Keep in mind this is a happy path and bad things can happen along a way. In particular: | ||
- creating command can fail: i.e. command can have incorrect params, or some params can be missing (command param validation) [400] | ||
- command execution can fail due to numerous reasons: | ||
- an object that we want act upon may not exist [404] | ||
- a user may not have permissions to perform certain command [403] | ||
- business rule may fail [400] | ||
- application level policy may fail, i.e. too many requests issued by the same user [429] | ||
- .... | ||
|
||
|
||
|
||
For example, query or command can be invalid, an , user may | ||
|
||
|
||
References: | ||
|
||
- [1] https://martinfowler.com/bliki/CQRS.html | ||
- [2] https://softwareengineering.stackexchange.com/questions/302187/crud-operations-in-ddd | ||
- [3] https://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ | ||
|
||
|
||
|
||
|
||
|
||
|
||
## Other references | ||
|
||
- https://github.com/VaughnVernon/IDDD_Samples/tree/master |
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,21 @@ | ||
# 1. Record architecture decisions | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
As the project is an example of a more advanced monolith architecture, it is necessary to save all architectural decisions in one place. | ||
|
||
## Decision | ||
|
||
For all architectural decisions Architecture Decision Log (ADL) is created. All decisions will be recorded as Architecture Decision Records (ADR). | ||
|
||
Each ADR will be recorded using [Michael Nygard template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions), which contains following sections: Status, Context, Decision and Consequences. | ||
|
||
## Consequences | ||
|
||
All architectural decisions should be recorded in log. Old decisions should be recorded as well with an approximate decision date. New decisions should be recorded on a regular basis. |
22 changes: 22 additions & 0 deletions
22
docs/architecture_decision_log/002_use_modular_monolith.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,22 @@ | ||
# 2. Use Modular Monolith System Architecture | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
An example of Modular Monolith architecture and tactical DDD implementation in Python is missing on the internet. | ||
|
||
## Decision | ||
|
||
We decided to create nontrivial application using Modular Monolith architecture and Domain-Driven Design tactical patterns. | ||
|
||
## Consequences | ||
|
||
- All modules must run in one single process as single application (Monolith) | ||
- All modules should have maximum autonomy (Modular) | ||
- DDD Bounded Contexts will be used to divide monolith into modules | ||
- DDD tactical patterns will be used to implement most of modules |
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,19 @@ | ||
# 3. Use .NET Core and C# language | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
As it is monolith, only one language must be selected for implementation. | ||
|
||
## Decision | ||
|
||
We decided to use Python 3.9 as it is the newest stable Python release at the moment of writing this document. The choice of web framework is deferred at this point. | ||
|
||
## Consequences | ||
|
||
- Whole application will be implemented in Python |
35 changes: 35 additions & 0 deletions
35
docs/architecture_decision_log/004_divide_system_into_2_modules copy.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,35 @@ | ||
# 4. Divide the system into 4 modules | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
The "Auction" domain contains 3 main subdomains: Bidding (core domain), Catalog (supporting subdomain), and User Access (generic domain). | ||
|
||
Since we decided to use Modular Monolith, all subdomains should be implemented as modules within a single system. | ||
|
||
## Possible solutions | ||
1. Create one "Auction" module and divide it into sub-modules. This solution is simpler to implement at the beginning. We do not have to set module boundaries and think how to communicate between them. On the other hand, this causes a lack of autonomy and can lead to Big Ball Of Mud anti-pattern. | ||
2. Create 3 modules based on Bounded Contexts which in this scenario maps 1:1 to domains. This solution is more difficult at the beginning. We need to set modules boundaries, communication strategy between modules and have more advanced infrastructure code. It is a more complex solution. On the other hand, it supports autonomy, maintainability, readability. We can develop our Domain Models in all of the Bounded Contexts independently. | ||
|
||
## Decision | ||
|
||
Solution 2. | ||
|
||
We created 3 modules: Bidding, Catalog, User Access. The key factor here is module autonomy and maintainability. We want to develop each module independently. This is more cleaner solution. It involves more work at the beginning but we are ready to pay this price. | ||
|
||
## Consequences | ||
- We can implement each module/Bounded Context independently. | ||
- We need to set clear boundaries between modules and communication strategy between modules (and implement them) | ||
- We need to define the API of each module | ||
- The API/GUI layer needs to know about all of the modules | ||
- We need to create shared libraries/classes to limit boilerplate code which will be the same in all modules | ||
- Complexity of the whole solution will increase | ||
- Complexity of each module will decrease | ||
- We will have clear separation of concerns | ||
- In addition to the application, we must divide the data | ||
- We can delegate development of particular module to defined team, work should be done without any conflicts on codebase |
33 changes: 33 additions & 0 deletions
33
docs/architecture_decision_log/005_separate_commands_and_queries.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,33 @@ | ||
# 4. Separate commands and queries | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
We want to keep controllers as thin as possible. Therefore each controller should have access to a module, which exposes an interface allowing to read system state using `queries` and change system state using `commands`. However, keep in mind that this does not imply having separate read and write models (as in pure CQRS) - it's up to a module architecture if same or different models are needed. | ||
|
||
``` | ||
def get_route_controller(request, module): | ||
module.execute_query(MyQuery( | ||
foo=request.GET.foo, | ||
bar=request.GET.bar, | ||
)) | ||
return Response(HTTP_200_OK) | ||
def post_route_controller(request, module): | ||
result = module.execute_command(MyCommand( | ||
foo=request.POST.foo, | ||
bar=request.POST.bar, | ||
)) | ||
return Response(HTTP_200_OK) | ||
``` | ||
|
||
|
||
## Consequences | ||
- controllers are thin | ||
- each module must implement `execute_query` and `execute_command` methods |
37 changes: 37 additions & 0 deletions
37
docs/architecture_decision_log/006_error_handling_during_operation_execution.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,37 @@ | ||
# 4. Error handling during operation execution | ||
|
||
Date: 2021-05-25 | ||
|
||
## Status | ||
|
||
Pending | ||
|
||
## Context | ||
|
||
When executing an operation (command or query), many bad things can happen. This can be related to access permission, data validation, business rule validation, infrastructure errors, etc. We need to collect and handle such errors in an unified way for further processing (API responses, UI notifications for a user). The solution should also support a way to convert errors into meaningful messages for an end user (including language translations). | ||
|
||
## Possible solutions | ||
1. Raise an exception during operation processing. This solutioun relies on throwing an exception during command execution and handling it somewhere upper in the call stack. | ||
|
||
Pros: | ||
- can throw exception from anywhere | ||
|
||
Cons: | ||
- non-linear program flow can become a mess really quickly because it’s hard to trace all existing connections between throw and catch statements. | ||
|
||
2. Explicitly return values indicating success or failure of an operation instead of throwing exceptions using a `Result` class. Expected erros should be reported to a `Result` object. Unexpected errors should be handled using exceptions. | ||
|
||
Pros: | ||
- can return multiple errors within one operation | ||
|
||
Cons: | ||
- ... | ||
|
||
## Decision | ||
|
||
TBA | ||
|
||
|
||
## Consequences | ||
|
||
.... |
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,48 @@ | ||
from modules.catalog.application.commands import ( | ||
CreateAuctionItemCommand, | ||
UpdateAuctionItemCommand, | ||
DeleteAuctionItemCommand, | ||
PublishAuctionItemCommand, | ||
UnpublishAuctionItemCommand, | ||
) | ||
from modules.catalog.domain.entities import AuctionItem | ||
from seedwork.application.commands import Command | ||
from seedwork.application.command_handlers import CommandResult | ||
from seedwork.application.decorators import command_handler | ||
|
||
# @unit_of_work | ||
@command_handler | ||
def create_auction_item(command: CreateAuctionItemCommand, repository) -> CommandResult: | ||
item = AuctionItem(**command.dict()) | ||
try: | ||
repository.insert(item) | ||
except: | ||
return CommandResult.fail("Failed to add item") | ||
return CommandResult.succed(id=item.id) | ||
|
||
|
||
# @unit_of_work | ||
# @command_handler | ||
def update_auction_item(command: UpdateAuctionItemCommand, repository): | ||
item = repository.find_by_id(command.id) | ||
item.title = command.title | ||
item.description = command.description | ||
repository.update(item) | ||
|
||
|
||
# @unit_of_work | ||
# @command_handler | ||
def delete_auction_item(command: DeleteAuctionItemCommand, repository): | ||
repository.delete(command.id) | ||
|
||
|
||
def publish_in_catalog(command: PublishAuctionItemCommand): | ||
pass | ||
|
||
|
||
def unpublish_from_catalog(command: UnpublishAuctionItemCommand): | ||
pass | ||
|
||
|
||
def xyz_view(request): | ||
publish_in_catalog(request.POST["id"]) |
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,25 @@ | ||
from seedwork.application.commands import Command | ||
from seedwork.domain.value_objects import Currency | ||
|
||
|
||
class CreateAuctionItemCommand(Command): | ||
title: str | ||
description: str | ||
price: Currency | ||
|
||
|
||
class UpdateAuctionItemCommand(Command): | ||
title: str | ||
description: str | ||
|
||
|
||
class DeleteAuctionItemCommand(Command): | ||
pass | ||
|
||
|
||
class PublishAuctionItemCommand(Command): | ||
pass | ||
|
||
|
||
class UnpublishAuctionItemCommand(Command): | ||
pass |
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,14 @@ | ||
from typing import Any | ||
from seedwork.domain.entities import Entity | ||
from seedwork.domain.value_objects import Currency | ||
from .rules import AuctionItemPriceMustBeGreaterThanZero | ||
|
||
|
||
class AuctionItem(Entity): | ||
title: str | ||
description: str | ||
price: Currency | ||
|
||
def __init__(self, **data: Any) -> None: | ||
super().__init__(**data) | ||
self.check_rule(AuctionItemPriceMustBeGreaterThanZero(self.price)) |
File renamed without changes.
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,2 @@ | ||
class ListingRepository: | ||
pass |
File renamed without changes.
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,12 @@ | ||
# EditorConfig is awesome: https://EditorConfig.org | ||
|
||
# top-most EditorConfig file | ||
root = true | ||
|
||
[*] | ||
indent_style = space | ||
indent_size = 4 | ||
end_of_line = lf | ||
charset = utf-8 | ||
trim_trailing_whitespace = false | ||
insert_final_newline = false |
Oops, something went wrong.