Chapter 4 – Get Your Hands Dirty on Clean Architecture

Chapter 4

Implementing a Use Case

Let's finally look at how we can manifest the architecture we have discussed in actual code.

Since the application, web, and persistence layers are so loosely coupled in our architecture, we are totally free to model our domain code as we see fit. We can do DDD, we can implement a rich or an anemic domain model, or we can invent our own way of doing things.

This chapter describes an opinionated way of implementing use cases within the hexagonal architecture style that we have introduced in the previous chapters.

As is fitting for a domain-centric architecture, we will start with a domain entity and then build a use case around it.

Implementing the Domain Model

We want to implement the use case of sending money from one account to another. One way to model this in object-oriented fashion is to create an Account entity that allows us to withdraw and deposit money so that we can withdraw money from the source account and deposit it into the target account:

package buckpal.domain;

public class Account {

  private AccountId id;

  private Money baselineBalance;

  private ActivityWindow activityWindow;

  // constructors and getters omitted

  public Money calculateBalance() {

    return Money.add(

            this.baselineBalance,

            this.activityWindow.calculateBalance(this.id));

  }

  public boolean withdraw(Money, AccountId targetAccountId) {

  

    if (!mayWithdraw(money)) {

      return false;

    }

  

    Activity withdrawal = new Activity(

        this.id,

        this.id,

        targetAccountId,

        LocalDateTime.now(),

        money);

    this.activityWindow.addActivity(withdrawal);

    return true;

  }

  

  private boolean mayWithdraw(Money money) {

    return Money.add(

        this.calculateBalance(),

        money.negate())

        .isPositive();

  }

  public boolean deposit(Money, AccountId sourceAccountId) {

    Activity deposit = new Activity(

        this.id,

        sourceAccountId,

        this.id,

        LocalDateTime.now(),

        money);

    this.activityWindow.addActivity(deposit);

    return true;

  }

}

The Account entity provides the current snapshot of an actual account. Every withdrawal from and deposit into an account is captured in an Activity entity. Since it would not be wise to always load all activities of an account into memory, the Account entity only holds a window of the last few days or weeks of activities, captured in the ActivityWindow value object.

To still be able to calculate the current account balance, the Account entity additionally has the baselineBalance attribute, representing the balance the account had just before the first activity of the activity window. The total balance then is the baseline balance plus the balance of all activities in the window.

With this model, withdrawing and depositing money from and into an account is a matter of adding a new activity to the activity window, as is done in the withdraw() and deposit() methods. Before we can withdraw, we check the business rule that says that we cannot overdraw an account.

Now that we have an Account entity that allows us to withdraw and deposit money, we can move outward to build a use case around it.

A Use Case in a Nutshell

First, let's discuss what a use case actually does. Usually, it follows these steps:

  1. Takes input
  2. Validates business rules
  3. Manipulates the model state
  4. Returns output

A use case takes input from an incoming adapter. You might wonder why I didn't call this step "Validate input." The answer is that I believe use case code should care about the domain logic and we shouldn't pollute it with input validation. So, we will do input validation somewhere else, as we will see shortly.

The use case is, however, responsible for validating business rules. It shares this responsibility with the domain entities. We will discuss the distinction between input validation and business rule validation later in this chapter.

If the business rules were satisfied, the use case then manipulates the state of the model in one way or another, based on the input. Usually, it will change the state of a domain object and pass this new state to a port implemented by the persistence adapter to be persisted. A use case might also call any other outgoing adapters, though.

The last step is to translate the return value from the outgoing adapter into an output object, which will be returned to the calling adapter.

With these steps in mind, let's see how we can implement our "Send Money" use case.

To avoid the problem of broad services discussed in Chapter 1, What's Wrong with Layers?, we will create a separate service class for each use case instead of putting all use cases into a single service class.

Here's a teaser:

package buckpal.application.service;

@RequiredArgsConstructor

@Transactional

public class SendMoneyService implements SendMoneyUseCase {

  private final LoadAccountPort loadAccountPort;

  private final AccountLock accountLock;

  private final UpdateAccountStatePort updateAccountStatePort;

  @Override

  public boolean sendMoney(SendMoneyCommand command) {

    // TODO: validate business rules

    // TODO: manipulate model state

    // TODO: return output

  }

}

The service implements the incoming port interface, SendMoneyUseCase, and will call the outgoing port interface, LoadAccountPort, to load an account, and calls UpdateAccountStatePort to persist an updated account state in the database. The following figure gives a graphical overview of the relevant components:

Figure 4.1: A service implements a use case, modifies the domain model, and calls an outgoing port to persist the modified state

Let's take care of those // TODOs we left in the preceding code.

Validating Input

Now we are talking about validating input, even though I just claimed that it's not a responsibility of a use case class. I still think, however, that it belongs to the application layer, so this is the place to discuss it.

Why not let the calling adapter validate the input before sending it to the use case? Well, do we want to trust the caller to have validated everything as is needed for the use case? Also, the use case might be called by more than one adapter, so the validation would have to be implemented by each adapter and we might get it wrong or forget it altogether.

The application layer should care about input validation because, well, otherwise it might get invalid input from outside the application core, and this might cause damage to the state of our model.

But where to put the input validation if not in the use case class?

We will let the input model take care of it. For the "Send Money" use case, the input model is the SendMoneyCommand class we have already seen in the previous code example. More precisely, we will do it within the constructor:

package buckpal.application.port.in;

@Getter

public class SendMoneyCommand {

  

  private final AccountId sourceAccountId;

  private final AccountId targetAccountId;

  private final Money;

  

  public SendMoneyCommand(

          AccountId sourceAccountId,

          AccountId targetAccountId,

          Money money) {

      this.sourceAccountId = sourceAccountId;

      this.targetAccountId = targetAccountId;

      this.money = money;

      requireNonNull(sourceAccountId);

      requireNonNull(targetAccountId);

      requireNonNull(money);

      requireGreaterThan(money, 0);

  }

}

For sending money, we need the IDs of the source and target account and the amount of money that is to be transferred. None of the parameters must be null and the amount must be greater than zero. If any of these conditions is violated, we simply refuse object creation by throwing an exception during construction.

By making the fields of SendMoneyCommand final, we effectively make it immutable. So, once constructed successfully, we can be sure that the state is valid and cannot be changed to something invalid.

Since SendMoneyCommand is part of the use case API, it's located in the incoming port package. Thus, the validation remains in the core of the application (within the hexagon of our hexagonal architecture) but does not pollute the sacred use case code.

But do we really want to implement each validation check by hand when there are tools that can do the dirty work for us? In the Java world, the de facto standard for this kind of work is the Bean Validation API (https://beanvalidation.org/). It allows us to express the validation rules we need as annotations on the fields of a class:

package buckpal.application.port.in;

@Getter

public class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {

  

  @NotNull

  private final Account.AccountId sourceAccountId;

  @NotNull  

  private final Account.AccountId targetAccountId;

  @NotNull  

  private final Money;

  

  public SendMoneyCommand(

          Account.AccountId sourceAccountId,

          Account.AccountId targetAccountId,

          Money money) {

      this.sourceAccountId = sourceAccountId;

      this.targetAccountId = targetAccountId;

      this.money = money;

      requireGreaterThan(money, 0);

      this.validateSelf();

  }

}

The SelfValidating abstract class provides the validateSelf() method, which we simply call as the last statement in the constructor. This will evaluate the Bean Validation annotations on the fields (@NotNull, in this case) and throw an exception in the case of a violation. If Bean Validation is not expressive enough for a certain validation, we can still implement it by hand, as we did to check that the amount is greater than zero.

The implementation of the SelfValidating class might look like this:

package shared;

public abstract class SelfValidating<T> {

  private Validator;

  public SelfValidating(){

    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();

    validator = factory.getValidator();

  }

  protected void validateSelf() {

    Set<ConstraintViolation<T>> violations = validator.validate((T) this);

    if (!violations.isEmpty()) {

      throw new ConstraintViolationException(violations);

    }

  }

}

With validation located in the input model, we have effectively created an anti-corruption layer around our use case implementations. This is not a layer in the sense of a layered architecture, calling the next layer below, but is instead a thin, protective screen around our use cases that bounces bad input back to the caller.

The Power of Constructors

The preceding input model, SendMoneyCommand, puts a lot of responsibility on its constructor. Since the class is immutable, the constructor's argument list contains a parameter for each attribute of the class. And since the constructor also validates the parameters, it's not possible to create an object with an invalid state.

In our case, the constructor has only three parameters. What if we had more parameters? Couldn't we use the Builder pattern to make it more convenient to use? We could make the constructor with the long parameter list private and hide the call to it in the build() method of our builder. Then, instead of having to call a constructor with 20 parameters, we could build an object like this:

new SendMoneyCommandBuilder()

    .sourceAccountId(new AccountId(41L))

    .targetAccountId(new AccountId(42L))

    // ... initialize many other fields

    .build();

We could still let our constructor do the validation so that the builder cannot construct an object with invalid state.

Sounds good? Think about what happens if we have to add another field to SendMoneyCommandBuilder (which will happen quite a few times in the lifetime of a software project). We add the new field to the constructor and to the builder. Then, a colleague (or a phone call, an email, or a butterfly...) interrupts our train of thought. After the break, we go back to coding and forget to add the new field to the code that calls the builder.

We don't get a word of warning from the compiler about trying to create an immutable object in an invalid state. Sure, at runtime – hopefully in a unit test – our validation logic will still kick in and throw an error because we missed a parameter.

But if we use the constructor directly instead of hiding it behind a builder, each time a new field is added or an existing field is removed, we can just follow the trail of compile errors to reflect that change in the rest of the codebase.

Long parameter lists can even be formatted nicely, and good IDEs help with parameter name hints:

Figure 4.2: The IDE shows parameter name hints in parameter lists to help us to not get lost

So, why not let the compiler guide us?

Different Input Models for Different Use Cases

We might be tempted to use the same input model for different use cases. Let's consider the use cases "Register Account" and "Update Account Details." Both will initially need almost the same input, namely some account details such as a description of the account.

The difference is that the "Update Account Details" use case also needs the ID of the account to be able to update that specific account. And the "Register Account" use case might need the ID of the owner, so that it can assign it to him or her. So, if we share the same input model between both use cases, we'd have to allow a null account ID to be passed into the "Update Account Details" use case and a null owner ID to be passed into the "Register Account" use case.

Allowing null as a valid state of a field in our immutable command object is a code smell by itself. But more importantly, how are we handling input validation now? Validation has to be different for the register and update use cases since each needs an ID the other doesn't. We'd have to build custom validation logic into the use cases themselves, polluting our sacred business code with input validation concerns.

Also, what do we do if the account ID field accidentally has a non-null value in the "Register Account" use case? Do we throw an error? Do we simply ignore it? These are the questions maintenance engineers – including future us – will ask when seeing the code.

A dedicated input model for each use case makes the use case much clearer and also decouples it from other use cases, preventing unwanted side effects. It comes with a cost, however, because we have to map incoming data into different input models for different use cases. We will discuss this mapping strategy along with other mapping strategies in Chapter 8, Mapping between Boundaries.

Validating Business Rules

While validating input is not part of the use case logic, validating business rules definitely is. Business rules are the core of the application and should be handled with appropriate care. But when are we dealing with input validation and when are we dealing with business rule validation?

A very pragmatic distinction between the two is that validating a business rule requires access to the current state of the domain model while validating input does not. Input validation can be implemented declaratively, as we did with the @NotNull annotations, while a business rule needs more context.

We might also say that input validation is a syntactical validation, while a business rule is a semantical validation in the context of a use case.

Let's take the rule "the source account must not be overdrawn." By the definition above, this is a business rule since it needs access to the current state of the model to check whether the source and target accounts do exist.

In contrast, the rule "the transfer amount must be greater than zero" can be validated without access to the model and thus can be implemented as part of the input validation.

I'm aware that this distinction may be subject to debate. You might argue that the transfer amount is so important that validating it should be considered a business rule in any case.

The distinction above helps us, however, to place certain validations within the code base and easily find them again later on. It's as simple as answering the question of whether the validation needs access to the current model state or not. This not only helps us to implement the rule in the first place, but it also helps the future maintenance engineer to find it again.

So, how do we implement a business rule?

The best way is to do put the business rules into a domain entity as we did for the rule "the source account must not be overdrawn":

package buckpal.domain;

public class Account {

    

  // ...

  public boolean withdraw(Money, AccountId targetAccountId) {

    if (!mayWithdraw(money)) {

      return false;

    }

    // ...

  }

}

This way, the business rule is easy to locate and reason about, because it's right next to the business logic that requires this rule to be honored.

If it's not feasible to validate a business rule in a domain entity, we can simply do it in the use case code before it starts working on the domain entities:

package buckpal.application.service;

@RequiredArgsConstructor

@Transactional

public class SendMoneyService implements SendMoneyUseCase {

  // ...

  @Override

  public boolean sendMoney(SendMoneyCommand command) {

    requireAccountExists(command.getSourceAccountId());

    requireAccountExists(command.getTargetAccountId());

    ...

  }

}

We simply call a method that does the actual validation and throws a dedicated exception in the case that this validation fails. The adapter interfacing with the user can then display this exception to the user as an error message or handle it any other way that seems fit.

In the preceding case, the validation simply checks if the source and target accounts actually exist in the database. More complex business rules might require us to load the domain model from the database first and then do some checks on its state. If we have to load the domain model anyway, we should implement the business rule in the domain entities themselves, as we did with the rule "the source account must not be overdrawn" previously.

Rich versus Anemic Domain Model

Our architecture style leaves open how to implement our domain model. This is a blessing because we can do what seems right in our context, and a curse because we don't have any guidelines to help us.

A frequent discussion is whether to implement a rich domain model following the DDD philosophy or an "anemic" domain model. I'm not going to favor one of the two, but let's discuss how each of them fits into our architecture.

In a rich domain model, as much of the domain logic as possible is implemented within the entities at the core of the application. The entities provide methods to change state and only allow changes that are valid according to the business rules. This is the way we pursued with the Account entity previously.

Where is our use case implementation in this scenario?

In this case, our use case serves as an entry point to the domain model. A use case then only represents the intent of the user and translates it into orchestrated method calls to the domain entities, which do the actual work. Many of the business rules are located in the entities instead of the use case implementation.

The "Send Money" use case service would load the source and target account entities, call their withdraw() and deposit() methods, and send them back to the database. Actually, the use case would also have to make sure that no other money transfer to and from the source and target account is happening at the same time, to avoid overdrawing an account, but we'll skip this business rule for the sake of simplicity.

In an "anemic" domain model, the entities themselves are very thin. They usually only provide fields to hold the state and getter and setter methods to read and change it. They don't contain any domain logic.

This means that the domain logic is implemented in use case classes. They are responsible for validating business rules, changing the state of the entities, and passing them into the outgoing ports responsible for storing them in the database. The "richness" is contained within the use cases instead of the entities.

Both styles and any number of other styles can be implemented using the architecture approach discussed in this book. Feel free to choose the one that fits your needs.

Different Output Models for Different Use Cases

Once the use case has done its work, what should it return to the caller?

Similar to the input, it has benefits if the output is as specific to the use case as possible. The output should only include the data that is really needed for the caller to work.

In the preceding example code of the "Send Money" use case, we returned a boolean. This is the minimal and most specific value we could possibly return in this context.

We might be tempted to return a complete Account with the updated entity to the caller. Perhaps the caller is interested in the new balance of the account.

But do we really want to make the "Send Money" use case return this data? Does the caller really need it? If so, shouldn't we create a dedicated use case for accessing that data that can be used by different callers?

There is no right answer to these questions. But we should ask them to try to keep our use cases as specific as possible. When in doubt, return as little as possible.

Sharing the same output model between use cases also tends to tightly couple those use cases. If one of the use cases needs a new field in the output model, the other use cases have to handle this field as well, even if it's irrelevant for them. Shared models tend to grow for multiple reasons in the long run. Applying the single responsibility principle and keeping models separated helps to decouple use cases.

For the same reason, we might want to resist the temptation to use our domain entities as output models. We don't want our domain entities to change for more reasons than necessary. However, we will talk more about using entities as input or output models in Chapter 11, Taking Shortcuts Consciously.

What about Read-Only Use Cases?

Previously, we have discussed how we might implement a use case that modifies the state of our model. How do we go about implementing read-only cases?

Let's assume the UI needs to display the balance of an account. Do we create a specific use case implementation for this?

It's awkward to talk of use cases for read-only operations like this one. Sure, in the UI, the requested data is needed to implement a certain use case we might call "View Account Balance." If this is considered a use case in the context of the project, by all means we should implement it just like the other ones.

From the viewpoint of the application core, however, this is a simple query for data. So, if it's not considered a use case in the context of the project, we can implement it as a query to set it apart from the real use cases.

One way of doing this within our architecture style is to create a dedicated incoming port for the query and implement it in a "query service":

package buckpal.application.service;

@RequiredArgsConstructor

class GetAccountBalanceService implements GetAccountBalanceQuery {

  private final LoadAccountPort loadAccountPort;

  @Override

  public Money getAccountBalance(AccountId accountId) {

    return loadAccountPort.loadAccount(accountId, LocalDateTime.now())

        .calculateBalance();

  }

}

The query service acts just as our use case services do. It implements an incoming port we named GetAccountBalanceQuery and calls the outgoing port, LoadAccountPort, to actually load the data from the database.

This way, read-only queries are clearly distinguishable from modifying use cases (or "commands") in our codebase. This plays nicely with concepts such as Command-Query Separation (CQS) and Command-Query Responsibility Segregation (CQRS).

In the preceding code, the service doesn't really do any work other than passing the query on to the outgoing port. If we use the same model across layers, we can take a shortcut and let the client call the outgoing port directly. We will talk about this shortcut in Chapter 11, Taking Shortcuts Consciously.

How Does This Help Me Build Maintainable Software?

Our architecture lets us implement the domain logic as we see fit, but if we model the input and output of our use cases independently, we avoid unwanted side effects.

Yes, it's more work than just sharing models between use cases. We have to introduce a separate model for each use case and map between this model and our entities.

But use case-specific models allow for a crisp understanding of a use case, making it easier to maintain in the long run. Also, they allow multiple developers to work on different use cases in parallel without stepping on each other's toes.

Together with tight input validation, use case-specific input and output models go a long way toward a maintainable codebase.