Clean DDD lessons: presenters

George
Technical blog from UNIL engineering teams
8 min readOct 5, 2023
Photo by Rob Laughter on Unsplash

In this lesson we shall take a closer look at Presenters in Clean Architecture.

Where do Presenters fit in?

As we have already discussed elsewhere: one of the most important aspects of Clean Architecture (CA) is the fact that there is a clear separation of concern between Controller and Presenter. Flow of control in CA proceeds from a controller towards a use case, and finally, exits through a presenter. Important thing to remember, is that Controller is a primary adapter, where as Presenter is a secondary adapter. A very clear representation of the directionality of the flow in CA is, of course, given by Robert C. Martin himself in the diagram from his famous article.

Once we have realized the essential difference of Controller and Presenter with the respect to the hexagonal asymmetry of a CA application, we can also see that there can be no dependency of any kind between these two very different components. This point has been discussed at length in this post. Indeed, the only component which deals directly with Presenters is Use Case. It is a use case which, depending on the outcome of business logic processing of a specific business scenario, can decide to invoke this or that presentation method.

The overwhelming number of examples of CA implementation do not explicitly set up dedicated Presenters, delegating presentation logic to Controllers, usually with a help of a framework like Spring Web, for example. In what follows, we should provide justifications for a different approach, more compatible with the spirit of CA.

Why Presenters?

Clean Architecture is all about isolation. Different types of components from different layers of the application, have different, specific functions. Controller is responsible for processing the input from a user (or a client) and deciding on which particular use case to perform. Use Case is responsible for “interacting” (to use Robert C. Martin’s original terminology) with the Domain Entities layer, triggering actual business logic processing by aggregates. To perform its work Use Case must involve communication with one or several secondary adapters, which is, of course, done via output ports. These secondary adapters, which a use case requires to perform a business scenario, are different in nature. For instance, a use case will most certainly need to communicate with a persistence gateway to materialize one or several aggregate roots, to which it will delegate actual state modification. It may also call an adapter for communicating with external services (message queues, logging systems, etc.). But, by far, the most important secondary adapter a use case will call at some point or another is a presenter. And this is because it is only through the output port of a presenter that a use case can signal back to the user the result of the execution of the particular business scenario.

It is a sole responsibility of Presenter to present the results of business logic processing done by Use Case back to the user.

Presenter acts as a bridge between Use Case layer and View layer. Generally, a view (from the outermost layer of CA) cannot directly manipulate domain models, which is a prerogative of a use case. This is because, otherwise, there is a danger of leaking business functionality into a layer which is not sufficiently stable or protected enough to handle it.

In canonical CA, Robert C. Martin advises to pass, what he calls, “Response Models” from Use Case layer to Presenter layer. These response models, represent projections of domain models (entities and value objects) directly manipulated by a use case. They are essentially DTOs destined to convey the information needed for the presentation of the outcome of a use case, without actually exposing the domain models to outer layers.

Clean DDD presenters

We take a slightly different approach in Clean DDD. Constructing a set of response models in a use case, specifically tailored to a particular presentation method, seems like an extraneous responsibility for a use case. We esteem that it is acceptable to pass domain models from Use Case to Presenter which in turn will prepare the information for View based on the properties of these models. This of course, is only possible since, in Clean DDD, all domain model are immutable and do not emit any domain events from any of their business methods.

If handling domain models, Presenter, must ever only access read-only properties of the aforementioned models. Never calling any of the business methods of any entities.

Let’s examine a concrete example of how a presenter can be implemented following Clean DDD paradigm. We implement a very simple domain consisting of the following aggregates: Person and Invoice, and the following value objects: PersonId, Money . We then model a use case which will generate an invoice for a customer with a given ID for a given amount. For the reference, all of the code is available in this Gist.

Here is the implementation of the use case:

class GenerateInvoiceUseCase implements GenerateInvoiceInputPort {

private GenerateInvoicePresenterOutputPort presenter;
private PersistenceGatewayOutputPort gateway;

@Transactional
@Override
public void generateInvoiceForCustomer(UUID customerUuid, BigDecimal amount, String currencyCode) {

PersonId customerId;
Money invoiceAmount;
Invoice invoice;
Person customer;
boolean success = false;
try {

// process input arguments
try {
invoiceAmount = Money.of(amount, currencyCode);
customerId = PersonId.of(customerUuid);
} catch (InvalidDomainObjectError e) {
presenter.presentInvalidInputError(e);
return;
}

// perform business logic: generate new invoice
try {
invoice = Invoice.of(customerId, invoiceAmount, Instant.now());
} catch (InvoiceGenerationError e) {
presenter.presentInvoiceGenerationError(e);
return;
}

// save invoice
try {
gateway.saveInvoice(invoice);
} catch (PersistenceOperationError e) {
presenter.presentPersistenceOperationError(e);
return;
}

// we need customer's personal information for presentation,
// so we load it here
try {
customer = gateway.loadPersonById(customerId);
} catch (PersistenceOperationError e) {
presenter.presentPersistenceOperationError(e);
return;
}

} catch (Exception e) {
// catch any errors we may have forgotten to handle,
// especially needed during development and debugging
presenter.presentError(e);
return;
} finally {
// rollback the transaction if the use case did not succeed
if (!success) {
gateway.rollback();
}
}

// call presenter to present successful outcome of the use case
presenter.presentResultOfGeneratingInvoice(invoice, customer);

}
}

This is just an example so some functionality required by a typical use case, such as security checks, is not present. We have already discussed previously and at length the structure of a use case. Here we concentrate on the points specifically related to presentation.

  • Presenter’s output port, GenerateInvoicePresenterOutputPort, is being called on several occasions during the use case’s execution. More precisely, there is a call to present each type of a business error which may be encountered by the use case. We can have a problem with the format of the inputs (invalid or null UUID used as a customer ID, etc.). We may have a problem actually generating a new instance of Invoice aggregate. And we may encounter a problem while trying to persist the invoice into the database. In all these cases, the presenter will be called with the precise business error, which should provide a mechanism to access any data relevant to the error to be presented to the user (in the error view).
  • If everything went well according to the business scenario implemented by the use case, there is a call to the presenter to present information to the user signaling that an invoice has successfully been created. For this call, we pass to the presenter the domain entities themselves: Invoice and Person. It is these entities (and the value objects they contain) which will allow the presenter to provide proper information to the view: the user’s name, the invoice date, etc.

Here is the implementation of the presenter:

class GenerateInvoiceWebPresenter implements GenerateInvoicePresenterOutputPort {

private static final ZoneId ZONE_ID = ZoneId.of("Europe/Zurich");
private static final Locale LOCALE = new Locale("fr");
private static final DateTimeFormatter INVOICE_DATE_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy", LOCALE);
private static final String CUSTOMER_NAME_TEMPLATE = "%s, %s";

// get the view-model for this presentation
ViewModel viewModel = new ViewModel();

@Override
public void presentError(Exception e) {
viewModel.setError(e.getLocalizedMessage());
}

@Override
public void presentInvalidInputError(InvalidDomainObjectError e) {
viewModel.setError(e.getLocalizedMessage());
}

@Override
public void presentPersistenceOperationError(PersistenceOperationError e) {
viewModel.setError(e.getLocalizedMessage());
}

@Override
public void presentInvoiceGenerationError(InvoiceGenerationError e) {
viewModel.setError(e.getLocalizedMessage());
}

@Override
public void presentResultOfGeneratingInvoice(Invoice invoice, Person customer) {

try {
// set billing date in view-model formatted with specific timezone and locale formatter
viewModel.setFormattedInvoiceDate(invoice.getInvoiceDateTime().atZone(ZONE_ID).format(INVOICE_DATE_FORMATTER));

// set invoice amount in view-model formatted using appropriate currency format
viewModel.setFormattedInvoiceAmount(formatMoneyAmount(invoice.getInvoiceAmount()));

// set the name of the customer on the invoice
viewModel.setCustomerName(CUSTOMER_NAME_TEMPLATE.formatted(customer.lastName, customer.firstName));
} catch (Exception e) {
// do not propagate any exceptions to the caller
presentError(e);
}
}

private String formatMoneyAmount(Money amount) {
Currency currency = Currency.getInstance(amount.getCurrencyCode());
NumberFormat format = new DecimalFormat();
format.setCurrency(currency);
return "%s %s".formatted(format.format(amount.getAmount()), format.getCurrency().getSymbol());
}
}

Again this is just an example, but the idea behind Presenter is clearly visible.

  • Presenter only concerns itself with presentation logic. Presenter should never make any business decisions. In our example, its task is to set the values for customer’s name, the invoice date, and the invoice’s amount all properly formatted with respect of the Locale and Currency used in this particular presentation.
  • Presenter never calls any of the business methods of any of the entities passed to it from the use case. All access is strictly read-only.
  • Presenter never calls any other ports.
  • Presenter catches all exceptions which may happen during execution of the presentation logic and deals with them itself. It must not allow them to propagate back to the use case. From the point of view of Use Case, once it has called Presenter signaling a successful outcome, the business scenario is considered as completed and all state is persisted. This should not change even it there is a problem with presentation.

Discussion

Following more canonical approach in CA, in the example above, one would create a Response Model POJO in the use case method containing the value objects needed for presentation (the first and last name of the customer, the Instant for the date of the invoice, the BigDecimal amount of the invoice, etc.) and pass them to the presenter. One can argue that picking this or that value object or property of a domain model for presentation should actually be the work of the presenter itself. This is why we allow passing domain models (immutable) to Presenter layer from Use Case layer in Clean DDD.

Another thing, which was somewhat skimmed over in the example, is the actual interaction with the view from the presenter. This will depend very heavily on the technology used for the view. For an interested reader, here are some examples of how this approach can be implemented coupled with Spring Web and REST, Spring MVC and Thymeleaf, ZK and MVVM, and PHP and Twig.

In the real-world application, presenters will most likely inherit from a common abstract class, one for each type of presentation: web, console, etc. Especially, such parent classes will handle error presentation in a uniform way. Here how this could be represented in UML for our example:

Using abstract superclass for common functionality of Presenters (error handling)

In this article we have looked in detail how Presenters are implemented following Clean DDD approach. Following the principles of Clean Architecture, Presenters are completely decoupled from Controllers and are used exclusively from Use Cases to perform presentation logic, such as: preparing result of successful or erroneous outcome of the execution of a particular business scenario to be presented to the user.

References

--

--