diff --git a/docs/core-concepts/changing-state.md b/docs/core-concepts/changing-state.md new file mode 100644 index 00000000..1c2cdc9a --- /dev/null +++ b/docs/core-concepts/changing-state.md @@ -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. diff --git a/docs/guides/change-state/application-services.md b/docs/guides/change-state/application-services.md new file mode 100644 index 00000000..be3fe25d --- /dev/null +++ b/docs/guides/change-state/application-services.md @@ -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 !} +``` diff --git a/docs/guides/change-state/index.md b/docs/guides/change-state/index.md index 2f774283..2584d64b 100644 --- a/docs/guides/change-state/index.md +++ b/docs/guides/change-state/index.md @@ -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 \ No newline at end of file diff --git a/docs_src/guides/change-state/008.py b/docs_src/guides/change-state/008.py new file mode 100644 index 00000000..59415ffd --- /dev/null +++ b/docs_src/guides/change-state/008.py @@ -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) diff --git a/mkdocs.yml b/mkdocs.yml index be58782c..52926a35 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,6 +81,7 @@ nav: - core-concepts/analysis-model.md - core-concepts/streams.md - core-concepts/identity.md + - core-concepts/changing-state.md - Architecture Patterns: - core-concepts/ddd.md - core-concepts/cqrs.md @@ -154,6 +155,7 @@ nav: - App Layer: - Changing State: - guides/change-state/index.md + - guides/change-state/application-services.md - guides/change-state/commands.md - guides/change-state/command-handlers.md - Persisting State: