Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 18

Building Application Services for User Use Cases

Recommended resources

Hello! Welcome back to our course on Domain-Driven Design in Python.

In our last lesson, we focused on implementing stateless Domain Services to encapsulate complex business rules that don't neatly fit within a single entity. We saw how they act as keepers of core domain logic, operating on domain objects passed to them.

Today, we shift our focus one layer up in the architecture. We will learn how to implement an Application Service. This component acts as the primary entry point for any user-facing action or use case. It is the orchestrator that directs the domain model to perform its work.

Our learning outcome for this session is to: Implement an Application Service to handle a specific user-facing use case by coordinating domain objects and repositories.

By the end of this lesson, you'll understand how to create a thin service layer that translates incoming requests into actions on your domain model, keeping your core logic clean and your application's features clearly defined.

1. The Role of the Application Service in Clean Architecture

Before we write any code, it's crucial to understand where the Application Service fits within the overall architecture. In DDD, we often use principles from Clean Architecture (or Hexagonal Architecture) to structure our application in layers. The Application Service is a key component of the "Application Layer."

Its job is not to perform business logic but to orchestrate it. Think of it as a project manager: it doesn't do the work itself, but it knows who to call (which domain objects), what information they need, and what to do with the result.

This video provides an excellent conceptual overview of this orchestration role.

Clean Architecture vs Domain-Driven Design (DDD) - Understand the Difference

This segment from the video 'Clean Architecture vs Domain-Driven Design (DDD)' by Amichai Mantinband uses a banking example to illustrate the responsibilities of the Application Layer. It clearly shows how it coordinates a use case.

Please watch the section 'Application Layer's Role in Use Case Orchestration' from 00:03:57 to 00:06:09. Focus on how the service orchestrates the 'open bank account' use case by calling other services and interacting with the database via interfaces.

As the video explains, the typical workflow orchestrated by an Application Service is:

  1. Receive a request: It takes simple input data, often packaged in a Command object (a simple data structure).
  2. Fetch the domain object(s): It uses a repository to retrieve the necessary Aggregate Root(s) from the persistence layer (e.g., a database).
  3. Execute the business logic: It calls a method on the Aggregate Root, passing any required data. All the business rules and invariants are checked inside the domain model, not in the Application Service.
  4. Persist the result: It uses the repository again to save the modified Aggregate Root.
  5. Return a result (optional): It might return a confirmation, an ID, or nothing at all.

This keeps the Application Service simple, procedural, and free of domain logic, making it easy to understand and test.


2. Implementing an Application Service in Python

Now, let's see what this looks like in code. We'll examine a concrete example of an Application Service responsible for the use case "sign a player for a team."

The following resource provides a clear and concise implementation.

Python Applications with DDD

The article 'Python Applications with DDD' shows a practical implementation of an Application Service. We will focus on the SignPlayer service.

Please read the section titled 'Application features'. It contains a single code block for the SignPlayer class. Focus on the sequence of operations inside the execute method.

Let's analyze the structure of the SignPlayer service from the article.

# A slightly simplified version for clarity
from src.domain.team.team import Team
from src.domain.team.team_repository import TeamRepository
from src.application.sign_player.sign_player_command import SignPlayerCommand

class SignPlayer:
    def __init__(self, team_repository: TeamRepository):
        self.team_repository = team_repository

    def execute(self, command: SignPlayerCommand) -> None:
        # 1. Fetch the aggregate
        team: Team = self.team_repository.find(command.team_id)

        if team is None:
            raise TeamNotFoundError('Team not found')

        # 2. Execute business logic on the aggregate
        team.sign_player(command.player_name)

        # 3. Persist the change
        self.team_repository.save(team)

        # (The original example also publishes events, which we'll cover later)

This code perfectly demonstrates the orchestration pattern:

  • Dependency Injection: The service is initialized with its dependencies—in this case, an implementation of TeamRepository. This follows the Dependency Inversion Principle, as it depends on an abstraction, not a concrete database class.
  • Command Object: The execute method accepts a SignPlayerCommand. This is a simple data-carrying object (likely a dataclass) that holds the team_id and player_name. Using a command object makes the service's interface explicit and stable.
  • The Orchestration Flow:
    1. self.team_repository.find(...): Retrieves the Team aggregate.
    2. team.sign_player(...): Delegates the actual work to the domain model. The Team entity itself contains the rules, like "a team cannot have more than eleven players." The Application Service doesn't know or care about these rules.
    3. self.team_repository.save(...): Saves the updated state of the Team aggregate.

The Application Service is the glue, but it's deliberately "dumb" about the business domain.


3. Connecting the Service to the Outside World

We've built the service, but how is it called? Application Services are the bridge between the "outside world" (like a web framework, a CLI, or a message queue consumer) and your domain model.

Let's look at how to wire this up in a web application using FastAPI.

Crafting Maintainable Python Applications with Domain ...

This example project from 'Crafting Maintainable Python Applications with Domain...' demonstrates how to use an Application Service within a FastAPI web application.

Please read the section 'Example Project: Auctioning Platform...'. Focus on the 'FastAPI Implementation' subsection, specifically the main.py file. Observe how the AuctionService is created and how the /create_auction endpoint uses it.

The main.py file in the example reveals the final piece of the puzzle:

  1. Composition Root: At the start of the application (main.py), the concrete dependencies are instantiated.

    # This is where we choose our concrete implementations
    auction_repo = AuctionRepository() 
    third_party_service = ThirdPartyService(...)
    auction_service = AuctionService(auction_repo, third_party_service)
    

    This setup, where dependencies are "wired up" in one place, is known as a Composition Root.

  2. Endpoint: The FastAPI endpoint defines the web-facing API.

    @app.post("/create_auction")
    async def create_auction(title: str, ...):
        # ...
    

    Its only job is to handle HTTP concerns: parse the request body, validate data types, and format the HTTP response.

  3. The Call: The endpoint then calls the Application Service to execute the use case.

    # The endpoint creates a domain object or command from the request data
    auction = Auction(title, start_date, end_date, initial_bid)
    # Then it calls the service
    await auction_service.create_auction(auction) 
    return {"message": "Auction created successfully."}
    

This structure creates a clean separation of concerns:

  • Infrastructure (FastAPI): Handles HTTP, serialization (JSON), etc.
  • Application (AuctionService): Orchestrates the use case.
  • Domain (Auction): Contains the actual business rules and state.


Conclusion

In this lesson, we've defined the role and implementation of the Application Service. It is the component that exposes your application's capabilities to the outside world, acting as a thin, coordinating layer on top of your rich domain model.

Key Takeaways:

  • An Application Service defines and orchestrates a single, user-facing use case.
  • It is the primary client of your domain model and repositories.
  • Its core responsibility follows a simple pattern: fetch an aggregate from a repository, call a method on the aggregate, and save the aggregate back to the repository.
  • Application Services should be stateless and contain no business logic. All rules and invariants belong in the domain layer (Entities, Value Objects, and Domain Services).
  • They are "wired up" to concrete infrastructure like web frameworks or databases at the application's entry point (the Composition Root).

Preview of the Next Lesson

Our SignPlayer and create_auction examples were simple; they only involved saving a single aggregate. But what happens if a use case needs to modify two different aggregates? For example, "transferring a player" might involve saving the old team and the new team. If the second save fails, our system is left in an inconsistent state.

In our next lesson, we will introduce the Unit of Work pattern, a powerful mechanism that allows us to group multiple repository operations into a single, atomic transaction, ensuring data consistency across complex use cases.