Clean DDD lessons: use cases

George
Technical blog from UNIL engineering teams
8 min readJul 30, 2023
Photo by Matt Benson on Unsplash

Clean Architecture revolves around an important concept — a use case. There is, however, quite a bit of confusion with relation to the role and the composition of use cases. We have previously wrote about the flow of control through a use case and the role of use cases in maintaining inter-aggregate invariants. In this article we shall reiterate some of the guidelines which one must keep in mind when designing a use case.

Role of use cases in CA

Use Case layer constitutes the single most salient feature which differentiates Clean Architecture from Hexagonal (Port and Adapters) architecture. In its original conception by Robert C. Martin, a use case (or “interactor”, how he calls them) orchestrates the logic of a concrete business scenario on the behalf of a specific user. A use case is executed from a controller, it may access any external services using any of the output ports available to it, it often loads one or several aggregates and invokes business logic on them. Finally, use case calls a presenter to present a successful outcome or to present a specific business error.

It is important to realize that a use case should not be thought of as a function or a domain service. And this for two very good reasons.

  • Unlike functions or domain services, a use case does not return anything to the caller (a controller). The flow of control leaves a use case exclusively through a call to an output port. It is usually a presenter — a component completely separate and independent of a controller — which is responsible to process the result of the execution of the use case.
  • Use case is not generic, and often not reused more than once in an application. On the contrary, each use case addresses a very specific business scenario, most likely, on the behalf of a very specific user.

So it is much more fruitful to conceptualize Use Case as a dispatcher or a switchboard operator. The flow of control comes into a use case from a controller, and the use case dispaches the flow to any output ports needed to perform its very specific business logic. The flow leaves the use case, temporarily — as in the case of accessing the persistence gateway, for example — or definitively when the use case completes and calls the presenter.

Lets look in more details at the inner-workings of a typical use case. Below we schematized the flow of control through a use case.

Successful and exceptional flow of control in a use case together with transactional demarcation

Calling a use case

Controller obtains an instance of a use case by looking up a prototype bean from the Dependency Injection (DI) container. This allows to externalize the configuration and the wiring of the collaborators of the use case, especially— the presenter — outside of a controller, keeping the responsibility of the controller to the minimum.

Always obtain a new, fully-wired, instance of a use case bean from DI container in a controller. Use prototype scoped beans, do not share the same instance of a use case between different executions of the same controller method.

Controller executes a use case by calling a business method defined by the input port of the use case. The granularity of a use case — the number of related business methods in an input port — can vary from one application to another.

Use clear, unambiguous, and precise names, inspired by Ubiquitous Language of the domain, when naming input ports and individual methods of a use case.

Successful and exceptional outcomes of a use case

If there is one thing which we need to keep in mind when designing a use case it is this: each use case will explicitly have to deal with one or several successful outcomes and one or more exceptional outcomes. With the number of exceptional outcomes usually much larger than the number of the successful outcomes. Each outcome of a use case is a result of the execution of a particular flow of control passing trough a succession of checkpoints (in the code). On the diagram above, we have indicated one such successful flow with thick green arrow and multiple possible exceptional flows as dashed thin red lines.

The number of checkpoints is usually more than one, but it should be kept to the minimum necessary to implement the logic of the specific use case. At each checkpoint, it is a sole prerogative of the use case at hand to deal with any exceptions arising from the execution of the code of a checkpoint. Only a use case is competent enough to deal with such and such business exception which may result from executing a call to an output port or a business method of an aggregate root. In fact, the logic of the use case which decides how to deal with this or that business exception or how to proceed with a successful control flow is the very raison d’être of the use case.

A use case either completes with a success or it deals with an exception by doing either one or both of the following two things:

  • It may call an output port (of a secondary adapter) to perform some useful operation: persist a new entity, send a notification, or log a message, for example.
  • It may call a presenter to present the result of the use case to the end-user.

Whatever flow a use case ends up executing, it must not throw any exceptions. This is because no component (up the caller’s stack from the use case) has any means to actually deal with these exceptions: for example, no other component, than the use case, has access to a presenter to be able to present the error to the user.

A use case must not throw any exceptions or propagate them up the caller’s stack.

Typical checkpoints of a use case

Lets now look in more details of the different types of checkpoints on the flow of control through a use case.

Depicted above as “checkpoint 1”, this is the place where we convert all input parameters (usually some primitives or flat DTOs) passed to the input port by the calling controller into a value objects understandable by our domain. In the process of this conversion, an implicit validation of inputs will take place. If inputs are invalid, an appropriate exceptional flow will be activated, presenting the user with contextualized error specific to the use case at hand. In this case we may even need to gather any relevant additional information (through calls to any of the output ports of the use case) to be provided to the presenter of the error.

Deal with input validation in Use Cases layer (not in controllers). This will allow for a uniform error handling orchestrated in a single place — a use case — for all errors related to each specific business scenario.

At this point, a typical use case will proceed to obtain one or more aggregate roots from a persistence gateway (using the corresponding output port, of course). There are numerous things which may go wrong at this point. They will be signaled as such by any errors coming from the gateway. A specific sub-type of each such error will trigger a specific exceptional flow. The end user, may need to be notified differently if there is a problem with a connection to the database or if the required entity could no longer be found there, for example.

Make sure there is a fine-grained hierarchy of business errors wrapping any specific technical exceptions coming from Interface Adapters layer into a use case. Names of business errors must closely reflect Ubiquitous Language.

Proceeding now to the “checkpoint 2”, a use case will perform its most important logic. It will most likely access any of the output ports available to it in order to gather all information required to assert any of the inter-aggregate invariants according to the concrete business scenario at hand. Once this information is gathered and all the inter-aggregate invariants are asserted, the use case will invoke any necessary business methods on any of the aggregate roots.

Delegate most of the domain logic processing from a use case to the necessary aggregates taking advantage of the power encapsulated into well-designed domain models.

One of the interesting invariants a use case may concern itself at this point, is checking that a specific user is authenticated into the system and that the user is authorized to access any of the involved aggregates or to perform the required business operation on them.

This is the place where the use case will also deal with any inconsistencies between the states of different but related aggregates. And this in a strongly consistent, synchronous fashion. Again, many different business errors will be dealt with accordingly via calls to output ports and/or Presenter.

Closer to the end of the execution of a use case, the control will enter the “checkpoint 3” where any of the modified or new aggregate instances need to be persisted back to the database.

What it is left, is what depicted as the “checkpoint 4” on the diagram. This is where we need to prepare any of the objects (“Response Models” in CA’s parlance) which will be sent to the presenter. It should be a straightforward conversion of any of the available domain models into data structures suitable for presentation.

Transactional demarcation

Clean DDD approach calls for a transactional demarcation possibly spanning several aggregates involved in the use case. This is the way a use case can guarantee any of inter-aggregate invariants in a strongly consistent fashion.

It is important to realize that the transactional boundary is not simply around the whole use case, but should only encompass the necessary checkpoints in the flow of control where the persistent state of the system is concerned. In the diagram above, this boundary is depicted by a gray box around checkpoints 1 to 3, as an illustration.

Discussion

A use case in CA is a focused procedure consistent of a sequence of steps intended to perform a concrete business scenario usually on behalf of a particular end-user of the application. A use case must provide both for any successful and any exceptional outcomes of its execution via uniformed error handing mechanism involving output ports (and especially the presenter). The use case may deal with several aggregates at once and must guarantee the overall consistency of the system state.

References

  1. A relatively complete example of a use case implemented for Library domain.

--

--