“Enchilada!” pattern by Uncle Bob

George
Technical blog from UNIL engineering teams
7 min readMay 29, 2024
Photo by Aleksandra Gencheva on Unsplash

Or why we should not return anything to Controller from Use Case when following Clean Architecture paradigm.

The slide

Here is a screenshot of the famous talk by Robert C. Martin (Uncle Bob) from NDC 2013 London conference: “Architecture — The Lost Years”. There is a slide in it with an intriguing title: “Enchilada!”. It is a great name, because, in this one slide, he showed the gist of his approach which became known as Clean Architecture.

Robert C. Martin, at NDC 2013, talking about Clean Architecture, video from https://vimeo.com/ndcconferences

A little note on terminology. Robert C. Martin calles Use Case — “Interactor” and Input/Output Ports — “Boundaries”.

There is a wealth of commentary which has appeared on Internet related to the this pattern. In this article we shall concentrate specifically on the question of relationship between Controller and Presenter. We have written about this precise point already in other articles on this blog. Here is also a relatively well-known and quite excellent discussion on Software Engineering StackExchange which deals specifically with the same point.

So, as rightfully mentioned in the commentary to the StackExchange post, there is absolutely no lines or arrows of any sort between Controller and Presenter boxes on the Uncle Bob’s slide above. Neither there are any lines between Controller and the (output) Boundary, which is the interface the Presenter implements. None whatsoever. And this means that:

  • Controller does not create (an instance of) Presenter
  • Controller is not injected with Presenter
  • Controller does not pass Presenter to Use Case (method)
  • Controller does not call Presenter (to present results of Use Case processing)

In fact, Controller does not know anything at all about Presenter. And this, of course, is logical. In Clean Architecture, Controller is an instance of Primary (Driving) Adapter and Presenter is an instance of Secondary (Driven) adapter.

Nevertheless, the overwhelming majority of examples of Clean Architecture implementations do not follow the strict separation of Controllers and Presenters. This is mostly due to some technical shortcuts related to Request-Response processing by a specific MVC framework used by the application. To separate technically Controller and Presenter, even when still using the MVC framework (like the popular Spring Web), is not very difficult. We have shown some ways of doing it on our other articles in this blog. Here we shall discuss why this is such an important point.

Leaking business logic to Controller

The most important idea about Clean Architecture is, of course, the idea of separation of concerns. Controller’s responsibility must be limited to the decision about which Use Case to invoke. That’s it. After that its work is done. It is Use Case, and use case alone, which performes the business logic of the application. Of course, the use case will offload quite a bit of business logic processing (i.e. intra-aggregate invariants checking) to Domain Entities. And that’s perfectly desirable. What important is that all business decision making is confined solely to the the two innermost layers of the application — Use Cases and Domain Entities layers.

Each use Case, being the main orchestrator of the business logic specific to the scenario it deals with, must invariable account for several control flows. There are one or several successful flows and one or several exceptional flows through each use case. Let’s see what happens if we are not separating Controller and Presenter while dealing with different outcomes of a use case execution.

Leaking business logic in Controller due to processing all possible outcomes of Use Case

What happens is that we are obliged to mix two different types on objects returned from a use case. We usually create a sort of wrapper object which either contains a domain error (an exception) or a DTO with successful results of the use case processing. Here is how such typical setup looks like in code.

WARNING: In our opinion, the code below does not follow “clean” separation of concerns between Controller and Presenter.

First a hierarchy of business errors:

/**
* Superclass for the hierarchy of our business exceptions.
*/
class BusinessError extends RuntimeException {
}

/**
* Error thrown from Domain Entities layer when some aggregate
* invariant is broken.
*/
class InvalidDomainError extends BusinessError {
}

/**
* Error thrown from the gateway (Interface Adapters layer) if there
* was a problem with persistence.
*/
class PersistenceError extends BusinessError {
}

Then, Outcome Value Object — a wrapper for a business error or a successful result (a DTO):

/**
* Value object representing an outcome of a use case: either an error
* or a successful result.
*/
@Value
@Builder
class Outcome<E extends BusinessError, R> {

E error;
R result;

public boolean hasError() {
return error != null;
}
}

Here are input port and the implementation of our use case:

/**
* Input port for some use case.
*/
interface SomeInputPort {
Outcome<? extends BusinessError, ?> executeLogicForSomeBusinessScenario(Object requestModel);
}

/**
* Use Case implementation, the only place where the business
* logic of some business scenario should be implemented.
*/
class SomeUseCase implements SomeInputPort {

@Override
public Outcome<? extends BusinessError, ?> executeLogicForSomeBusinessScenario(Object requestModel) {

try {
/*
Perform all the necessary steps of the use case,
according the requirements of the specific
use case scenario. Will most likely involve
calling aggregate roots and/or secondary ports
(e.g. the gateway). Return a successful outcome
to the controller if everything went as expected.
*/

return Outcome.builder()
.result("our DTO with results")
.build();

}
catch (InvalidDomainError | PersistenceError e){
// return an outcome with an error if something went wrong,
// but which we have anticipated
return Outcome.builder()
.error(e)
.build();
}
catch (Exception e) {
// repackage any unforeseen error to as a generic business error
// and return an outcome with the error
return Outcome.builder()
.error(new BusinessError(e))
.build();
}

}
}

Here is the output port (interface) for our Presenter with several presentation methods distinguished by the type of errors or successful results they are designed to present:

/**
* Output port for the presenter defining the interface for
* presenting errors and successful results of the use case
* logic processing.
*/
interface SomePresenterOutputPort {

void presentGenericBusinessError(BusinessError error);

void presentErrorInCaseOfInvalidInputOrOtherDomainInconsistencies(InvalidDomainError error);

void presentErrorInCaseOfProblemPersistingState(PersistenceError error);

void presentSuccessfulResultOfSomeBusinessScenario(Object result);
}

And here we show what happens in our controller when we get back from the use case and need to process the returned Outcome object:

/**
* Controller which processes "Outcome" returned from the use case.
*/
class SomeController {

// these maybe wired or created by the controller itself (it's not important)

SomeInputPort someUseCase;
SomePresenterOutputPort somePresenter;

// request handling method
void handleRequest(Object requestModel) {

// call use case and receive outcome

Outcome<? extends BusinessError, ?> outcome = someUseCase.executeLogicForSomeBusinessScenario(requestModel);

/*
Here we decide what to do depending on whether outcome is a success
or an error. And, moreover, we differentiate error presentation
depending on the nature of the error we may have received.
*/

if (outcome.hasError()) {
BusinessError error = outcome.getError();
if (error instanceof InvalidDomainError e) {
somePresenter.presentErrorInCaseOfInvalidInputOrOtherDomainInconsistencies(e);
} else if (error instanceof PersistenceError e) {
somePresenter.presentErrorInCaseOfProblemPersistingState(e);
} else {
somePresenter.presentGenericBusinessError(error);
}
} else {
// successful presentation
somePresenter.presentSuccessfulResultOfSomeBusinessScenario(outcome.getResult());
}

}
}

The important thing to notice here is that, invariably, we have leaked some of the business logic processing to the controller.

The “if-then-else” statement (in Controller) which must know exactly the nature of the the error returned from Use Case — in order to call an appropriate presentation logic — is, in fact, an integral part of the business scenario itself and, as such, must be confined exclusively to Use Case.

Pandora’s box

Once we allowed ourselves to do any business related logic processing in Controller it will be very difficult to stop. What will happen is that we shall be more and more tempted to do things like this in our controllers:

class SomeOtherController {

SomeInputPort someUseCase;
SomePresenterOutputPort somePresenter;
AnotherInputPort anotherUseCase;
AnotherPresenterOutputPort anotherPresenter;
SpecialInputPort specialInputPort;
SpecialPesenterOutputPort specialPresenter;

void handleRequest(Object requestModel) {

Outcome<? extends BusinessError, ?> outcome = someUseCase.executeLogicForSomeBusinessScenario(requestModel);

if (outcome.hasError()) {
somePresenter.presentGenericBusinessError(outcome.getError());
}
else {
Pair<Boolean, ?> someUseCaseResult = (Pair<Boolean, ?>) outcome.getResult();
if (someUseCaseResult.getKey()) {
// execute another use case only if some use case has succeeded
anotherUseCase.executeAnotherBusinessLogic(someUseCaseResult.getValue());
}
else {
anotherUseCase.executeYetAnotherBusinessLogic(someUseCaseResult.getValue());
}

// then, possibly, deal with the outcome of another use case in turn
}

}

}

Code above would be an example of how we may want to solve a difficult case of “chaining” calls to different use cases by introducing just a little bit more of business logic processing in Controller.

Conclusion

Of course, all the above code, is just an illustration of precisely what Uncle Bob does not want us to do. All of the business logic should instead be present solely in Use Case (with exception of intra-aggregate invariants checks in the Domain Entities layer). That includes even security assertions and other authorization logic, as well.

It is Use Case which, for a specific business scenario, decides what to do, how to handle any of the errors, and when/if to call any presentation methods on Presenter. Only this approach will guarantee the “cleanest” possible way to implement our application without business logic leaking into Interface Adapters layer (Controller).

References

--

--