Chapter 5 – Get Your Hands Dirty on Clean Architecture

Chapter 5

Implementing a Web Adapter

Most applications today have some kind of web interface – either a UI that we can interact with via a web browser or an HTTP API that other systems can call to interact with our application.

In our target architecture, all communication with the outside world goes through adapters. So, let's discuss how we can implement an adapter that provides such a web interface.

Dependency Inversion

The following figure gives a zoomed-in view of the architectural elements that are relevant to our discussion of a web adapter—the adapter itself and the ports through which it interacts with our application core:

Figure 5.1: An incoming adapter talks to the application layer through dedicated incoming ports, which are interfaces implemented by the application services

The web adapter is a "driving" or "incoming" adapter. It takes requests from the outside and translates them into calls to our application core, telling it what to do. The control flow goes from the controllers in the web adapter to the services in the application layer.

The application layer provides specific ports through which the web adapter can communicate. The services implement these ports and the web adapter can call these ports.

If we look closer, we notice that this is the Dependency Inversion Principle in action. Since the control flow goes from left to right, we could just as well let the web adapter call the use cases directly, as shown in the following figure:

Figure 5.2: We can remove the port interfaces and call the services directly

So, why do we add another layer of indirection between the adapter and the use cases? The reason is that the ports are a specification of the places where the outside world can interact with our application core. Having ports in place, we know exactly what communication with the outside world takes place, which is valuable information for any maintenance engineer working on your legacy codebase.

Having said that, one of the shortcuts we will talk about in Chapter 11, Taking Shortcuts Consciously, is just leaving the incoming ports out and calling the application services directly.

One question remains, though, which is relevant for highly interactive applications. Imagine an application that sends real-time data to the user's browser via web sockets. How does the application core send this real-time data to the web adapter, which in turn sends it to the user's browser?

For this scenario, we definitely need a port. This port must be implemented by the web adapter and called by the application core, as depicted in the following figure:

Figure 5.3: If an application must actively notify a web adapter, we need to go through an outgoing port to keep the dependencies in the right direction

Technically speaking, this would be an outgoing port and make the web adapter an incoming and outgoing adapter. But there is no reason that the same adapter cannot be both at the same time.

For the rest of this chapter, we will assume that the web adapter is an incoming adapter only since this is the most common case.

Responsibilities of a Web Adapter

What does a web adapter actually do? Let's say we want to provide a REST API for our BuckPal application. Where do the responsibilities of the web adapter start and where do they end?

A web adapter usually does these things:

  1. Maps HTTP requests to Java objects
  2. Performs authorization checks
  3. Validates input
  4. Maps input to the input model of the use case
  5. Calls the use case
  6. Maps the output of the use case back to HTTP
  7. Returns an HTTP response

First of all, a web adapter must listen to HTTP requests that match certain criteria, such as a certain URL path, HTTP method, or content type. The parameters and the content of a matching HTTP request must then be deserialized into objects we can work with.

Commonly, a web adapter then does an authentication and authorization check and returns an error if it fails.

The state of the incoming objects can then be validated. But haven't we already discussed input validation as a responsibility of the input model to the use cases? Yes, the input model to the use cases should only allow input that is valid in the context of the use cases. But here, we are talking about the input model to the web adapter. It might have a completely different structure and semantics from the input model to the use cases, so we might have to perform different validations.

I don't advocate implementing the same validations in the web adapter as we have already done in the input model of the use cases. Instead, we should validate that we can transform the input model of the web adapter into the input model of the use cases. Anything that prevents us from doing this transformation is a validation error.

This brings us to the next responsibility of a web adapter: to call a certain use case with the transformed input model. The adapter then takes the output of the use case and serializes it into an HTTP response, which is sent back to the caller.

If anything goes wrong on the way and an exception is thrown, the web adapter must translate the error into a message that is sent back to the caller.

That's a lot of responsibilities weighing on the shoulders of our web adapter. But it's also a lot of responsibilities that the application layer should not be concerned with. Anything that has to do with HTTP must not leak into the application layer. If the application core knows that we are dealing with HTTP on the outside, we have essentially lost the option to perform the same domain logic from other incoming adapters that do not use HTTP. In a good architecture, we want to keep our options open.

Note that this boundary between the web adapter and application layer comes naturally if we start development with the domain and application layers instead of with the web layer. If we implement the use cases first, without thinking about any specific incoming adapter, we are not tempted to blur the boundary.

Slicing Controllers

In most web frameworks – such as Spring MVC in the Java world – we create controller classes, which perform the responsibilities we have discussed previously. So, do we build a single controller that answers all the requests directed at our application? We don't have to. A web adapter can certainly consist of more than one class.

We should take care, however, to put these classes into the same package hierarchy to mark them as belonging together, as discussed in Chapter 3, Organizing Code.

So, how many controllers do we build? I say we should build too many rather than too few. We should make sure that each controller implements a slice of the web adapter that is as narrow as possible and that shares as little as possible with other controllers.

Let's take the operations on an account entity within our BuckPal application. A popular approach is to create a single AccountController that accepts requests for all operations that relate to accounts. A Spring controller providing a REST API might look like the following code snippet:

package buckpal.adapter.web;

@RestController

@RequiredArgsConstructor

class AccountController {

  

  private final GetAccountBalanceQuery getAccountBalanceQuery;

  private final ListAccountsQuery listAccountsQuery;

  private final LoadAccountQuery loadAccountQuery;

  

  private final SendMoneyUseCase sendMoneyUseCase;

  private final CreateAccountUseCase createAccountUseCase;

  

  @GetMapping("/accounts")

  List<AccountResource> listAccounts(){

    ...

  }

  @GetMapping("/accounts/id")

  AccountResource getAccount(@PathVariable("accountId") Long accountId){

    ...

  }

  

  @GetMapping("/accounts/{id}/balance")

  long getAccountBalance(@PathVariable("accountId") Long accountId){

    ...

  }

  

  @PostMapping("/accounts")

  AccountResource createAccount(@RequestBody AccountResource account){

    ...

  }

  @PostMapping("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")

  void sendMoney(

      @PathVariable("sourceAccountId") Long sourceAccountId,

      @PathVariable("targetAccountId") Long targetAccountId,

      @PathVariable("amount") Long amount) {

    ...

  }

}

Everything concerning the account resource is in a single class, which feels good. But let's discuss the downsides of this approach.

First, less code per class is a good thing. I have worked on a legacy project where the largest class had 30,000 lines of code. It was actually a conscious architecture decision (by our predecessors, mind you) that lead to there being 30,000 lines in a single class: to change the system at runtime, without re-deployment, it allowed us to upload compiled Java bytecode in a .class file. And it only allowed us to upload a single file, so this file had to contain all the code... .

That's no fun. Even if the controller only accumulates 200 lines of code over the years, it's still harder to grasp than 50 lines, even when it's cleanly separated into methods.

The same argument is valid for test code. If the controller itself has a lot of code, there will be a lot of test code. And, often, test code is even harder to grasp than production code, because it tends to be more abstract. We also want to make the tests for a certain piece of production code easy to find, which is easier in small classes.

What's equally important, however, is that putting all operations into a single controller class encourages the reuse of data structures. In the preceding code example, many operations share the AccountResource model class. It serves as a bucket for everything that is needed in any of the operations. AccountResource probably has an id field. This is not needed in the create operation and will probably cause confusion here more than it will help. Imagine that an Account has a one-to-many relationship with User objects. Do we include those User objects when creating or updating a book? Will the users be returned by the list operation? This is a simple example, but in any above-playsize project, we will ask these questions at some point.

So, I advocate the approach of creating a separate controller, potentially in a separate package, for each operation. Also, we should name the methods and classes as closely to our use cases as possible:

package buckpal.adapter.web;

@RestController

@RequiredArgsConstructor

public class SendMoneyController {

  private final SendMoneyUseCase;

  @PostMapping("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")

  void sendMoney(

      @PathVariable("sourceAccountId") Long sourceAccountId,

      @PathVariable("targetAccountId") Long targetAccountId,

      @PathVariable("amount") Long amount) {

    SendMoneyCommand command = new SendMoneyCommand(

        new AccountId(sourceAccountId),

        new AccountId(targetAccountId),

        Money.of(amount));

    sendMoneyUseCase.sendMoney(command);

  }

}

Also, each controller can have its own model, such as CreateAccountResource or UpdateAccountResource, or use primitives as input, as in the preceding example.

Those specialized model classes may even be private to the controller's package so they cannot accidentally be reused somewhere else. Controllers may still share models but using shared classes from another package makes us think about it more, and perhaps we will find out that we don't need half of the fields and create our own, after all.

Also, we should think hard about the names of the controllers and services. Instead of CreateAccount, for instance, wouldn't RegisterAccount be a better name? In our BuckPal application, the only way to create an account is for a user to register one. So, we use the word "register" in class names to better convey their meaning. There are certainly cases where the usual suspects Create..., Update..., and Delete... sufficiently describe a use case, but we might want to think twice before actually using them.

Another benefit of this slicing style is that it makes parallel work on different operations a breeze. We won't have merge conflicts if two developers work on different operations.

How Does This Help Me Build Maintainable Software?

When building a web adapter for an application, we should keep in mind that we are building an adapter that translates HTTP to method calls for the use cases of our application and translates the results back to HTTP and does not do any domain logic.

The application layer, on the other hand, should not do HTTP, so we should make sure not to leak HTTP details. This makes the web adapter replaceable by another adapter should the need arise.

When slicing web controllers, we should not be afraid to build many small classes that don't share a model. They are easier to grasp, to test, and support parallel work. It's more work initially to set up such fine-grained controllers, but it will pay off during maintenance.