-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add documentation for Application Services
- Loading branch information
Showing
5 changed files
with
214 additions
and
23 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,128 @@ | ||
# Changing State | ||
|
||
In DDD, changing the state of the system is a critical operation that is | ||
carefully controlled and structured. This section outlines the principles and | ||
practices that govern how state transitions occur within an application. | ||
|
||
It is essential for understanding how to maintain the integrity and consistency | ||
of your domain model while handling user-driven changes in a reliable and | ||
scalable way. | ||
|
||
## The Domain Model is protected | ||
|
||
The Domain Model is not accessible by the external world. | ||
|
||
One of the foundational principles in DDD is that the domain model — | ||
representing the core business logic and rules of your application — is | ||
protected from direct access by external systems or layers. This encapsulation | ||
ensures that the domain model remains pure, focused on business logic, and | ||
free from concerns about external interactions. | ||
|
||
The domain model should only be interacted with via specific interfaces | ||
designed to handle business operations, ensuring that all interactions with the | ||
model are controlled and follow the defined business rules. | ||
|
||
*In Protean, these "interfaces" are aggregate methods named in line with the | ||
ubiquitous language.* | ||
|
||
## Only the Application Layer talks to the Domain Model | ||
|
||
The application layer acts as the intermediary between the domain model and the | ||
rest of the system. It is the only layer that can directly invoke changes on | ||
the domain model. This separation of concerns ensures that the domain logic is | ||
only manipulated through well-defined use cases. | ||
|
||
This design allows for better maintainability and flexibility because the | ||
application layer changes at a different rate than the domain model. It also | ||
supports testing the domain model in isolation and the ability to refactor | ||
the domain logic to evolve independently from other system concerns. | ||
|
||
## Application Layer encloses actions | ||
|
||
The API layer often serves as the entry point for external requests. The API | ||
layer captures user inputs or requests and invokes the appropriate actions | ||
in the application layer. | ||
|
||
By having the API layer delegate actions to the application layer, we maintain | ||
a clear separation between external communication and internal processing. | ||
This approach not only protects the domain model from direct exposure but also | ||
allows the application to handle various concerns such as validation, | ||
authorization, and orchestration of complex workflows before interacting with | ||
the domain model. | ||
|
||
There are two ways the external API layer can invoke the application layer: | ||
|
||
### Use cases | ||
|
||
Application services are a common pattern in DDD, serving as a facade for | ||
business operations. When an API layer receives a request, it delegates the | ||
operation to the appropriate application service, which then coordinates the | ||
necessary actions to fulfill the request. | ||
|
||
Application services enclose and encapsulate business use cases, that are | ||
defined in the ubiquitous language. These use cases may or may not be reusable, | ||
but every business use case has a 1-1 mapping with a use case in application | ||
services. | ||
|
||
This design also allows for better organization of business logic, as each | ||
service is responsible for a specific set of related operations, reducing the | ||
overall complexity of the application layer. | ||
|
||
### Commands | ||
|
||
In systems that implement CQRS and Event Sourcing architecture patterns, the | ||
separation between command (write) and query (read) models is a key principle. | ||
When a user interacts with the system, the API captures their intent as a | ||
command — an explicit request to perform a specific operation — and submits it | ||
to the domain. | ||
|
||
By capturing intent as commands, the system can ensure that each operation is | ||
processed consistently, with a clear audit trail of how the system's state | ||
evolves over time. | ||
|
||
This separation also allows for optimized handling of write operations, focusing on | ||
modifying the state of the system, while queries are handled separately, | ||
optimized for reading data. | ||
|
||
Each Command is processed by a halder method in a Command Handler element. | ||
|
||
Once a command is submitted, it is processed by the command handler. Each | ||
command handler method contains the logic necessary to interpret the command, | ||
hydrate the relevant aggregate, and apply the appropriate changes to the domain | ||
model. | ||
|
||
Command handlers provide a clear and organized way to handle write operations. | ||
By isolating command processing in dedicated handlers, the system remains | ||
modular, with each handler focused on a specific aspect of the domain, | ||
improving both maintainability and scalability. | ||
|
||
## Application layer hydrates aggregates | ||
|
||
The Application Layer is responsible for retrieving (a.k.a hydrating) an | ||
aggregate from the persistence store (or an event store if using the Event | ||
Sourcing pattern), and then persisting it. | ||
|
||
When a command is processed, the application layer hydrates the aggregate and | ||
then invokes methods on the up-to-date aggregate. | ||
|
||
## Aggregates mutate | ||
|
||
Aggregates encapsulate business logic and ensure that all state transitions are | ||
valid. When an aggregate receives input through a command, it evaluates the | ||
request against its internal rules and invariants. If all conditions are met, | ||
the aggregate mutates — changing its state accordingly. | ||
|
||
In addition to mutating, aggregates can also raise events that represent | ||
significant changes in the system. These events can be used to trigger other | ||
processes or communicate state changes to external systems, leadig to richer, | ||
complex workflows. | ||
|
||
## Application layer persists mutated aggregates | ||
|
||
After the aggregate has mutated, the changes need to be persisted to ensure | ||
that the system's state is durable and consistent. Repositories save aggregates | ||
back to the persistence store or event store. | ||
|
||
Repositories not only persist the changes but also handle the publication of | ||
events raised by the aggregate. These events are stored in the event store, | ||
providing a detailed history of how the system's state has evolved over time. |
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,32 @@ | ||
# Application Services | ||
|
||
Application services act as a bridge between the external API layer and the | ||
domain model, orchestrating business logic and use cases without exposing the | ||
underlying domain complexity. They encapsulate and coordinate operations, | ||
making them reusable and easier to manage, ensuring that all interactions with | ||
the domain are consistent and controlled. | ||
|
||
## Key Facts | ||
|
||
- Application Services encapsulate business use cases and serve as the main | ||
entry point for external requests to interact with the domain model. | ||
- Application Services are predominantly used on the write side of the | ||
application. If you want to use them on the read side as well, it is | ||
recommended to create a separate application service for the read side. | ||
- Application Services are stateless and should not hold any business logic | ||
themselves; instead, they orchestrate and manage the flow of data and | ||
operations to and from the domain model. | ||
- Application Services ensure transaction consistency by automatically | ||
enclosing all use case methods within a unit of work context. | ||
- Application Services can interact with multiple aggregates and repositories, | ||
but should only persist one aggregate, relying on events for eventual | ||
consistency. | ||
|
||
## Defining an Application Service | ||
|
||
Application Services are defined with the `Domain.application_service` | ||
decorator: | ||
|
||
```python hl_lines="32 34 41" | ||
{! docs_src/guides/change-state/008.py !} | ||
``` |
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 |
---|---|---|
@@ -1,34 +1,19 @@ | ||
# Changing State | ||
|
||
## Persisting State | ||
In the [core concept on Changing State](../../core-concepts/changing-state.md), | ||
we discussed the workflow and thought process on how to accept state change | ||
requests and process them. | ||
|
||
- About Repositories and Repository Pattern | ||
In this section, we dive deeper into concrete implementations of the | ||
application layer. | ||
|
||
- Different available repositories | ||
- Repository Configuration | ||
- Automatic generation of repositories | ||
- [Application services and use cases](./application-services.md) | ||
- [Commands](./commands.md) | ||
- [Command Handlers](./command-handlers.md) | ||
|
||
### Basic Structure | ||
|
||
A repository provides three primary methods to interact with the persistence | ||
store: | ||
|
||
#### **`add`** - Adds a new entity to the persistence store. | ||
|
||
#### **`get`** - Retrieves an entity from the persistence store. | ||
|
||
- Persisting aggregates | ||
- Retreiving aggregates | ||
- Queries | ||
- Data Access Objects | ||
- Removing aggregates | ||
|
||
- Custom Repositories | ||
- Registering a custom Repository | ||
- Database-specific Repositories | ||
|
||
- Working with the Application Layer | ||
- Unit of Work | ||
|
||
`repository_for` | ||
Using repositories for filtering vs. for read-side operations |
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,44 @@ | ||
from protean import Domain, current_domain, use_case | ||
from protean.fields import Identifier, String | ||
|
||
auth = Domain(__file__, "Auth", load_toml=False) | ||
|
||
|
||
@auth.aggregate | ||
class User: | ||
email = String() | ||
name = String() | ||
status = String(choices=["INACTIVE", "ACTIVE", "ARCHIVED"], default="INACTIVE") | ||
|
||
@classmethod | ||
def register(cls, email: str, name: str): | ||
user = cls(email=email, name=name) | ||
user.raise_(Registered(user_id=user.id, email=user.email, name=user.name)) | ||
|
||
return user | ||
|
||
def activate(self): | ||
self.status = "ACTIVE" | ||
|
||
|
||
@auth.event(part_of=User) | ||
class Registered: | ||
user_id = Identifier() | ||
email = String() | ||
name = String() | ||
|
||
|
||
@auth.application_service(part_of=User) | ||
class UserApplicationServices: | ||
@use_case | ||
def register_user(self, email: str, name: str) -> Identifier: | ||
user = User.register(email, name) | ||
current_domain.repository_for(User).add(user) | ||
|
||
return user.id | ||
|
||
@use_case | ||
def activate_user(sefl, user_id: Identifier) -> None: | ||
user = current_domain.repository_for(User).get(user_id) | ||
user.activate() | ||
current_domain.repository_for(User).add(user) |
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