Skip to content

Commit

Permalink
Add documentation for Application Services
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Aug 20, 2024
1 parent da9681b commit 46b57cc
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 23 deletions.
128 changes: 128 additions & 0 deletions docs/core-concepts/changing-state.md
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.
32 changes: 32 additions & 0 deletions docs/guides/change-state/application-services.md
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 !}
```
31 changes: 8 additions & 23 deletions docs/guides/change-state/index.md
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
44 changes: 44 additions & 0 deletions docs_src/guides/change-state/008.py
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)
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 46b57cc

Please sign in to comment.