Ports and fat adapters by Mark Seemann
Is it worth it having a separate use-case layer?
When I occasionally post something about Ports and Adapters (also known as hexagonal architecture), a few reactions seem to indicate that I'm 'doing it wrong'. I apologize for the use of weasel words, but I don't intend to put particular people on the spot. Everyone has been nice and polite about it, and it's possible that I've misunderstood the criticism. Even so, a few comments have left me with the impression that there's an elephant in the room that I should address.
In short, I usually don't abstract application behaviour from frameworks. I don't create 'application layers', 'use-case classes', 'mediators', or similar. This is a deliberate architecture decision.
In this article, I'll use a motivating example to describe the reasoning behind such a decision.
Humble Objects #
A software architect should consider how the choice of particular technologies impact the development and future sustainability of a solution. It's often a good idea to consider whether it makes sense to decouple application code from frameworks and particular external dependencies. For example, should you hide database access behind an abstraction? Should you decouple the Domain Model from the web framework you use?
This isn't always the right decision, but in the following, I'll assume that this is the case.
When you apply the Dependency Inversion Principle (DIP) you let the application code define the abstractions it needs. If it needs to persist data, it may define a Repository interface. If it needs to send notifications, it may define a 'notification gateway' abstraction. Actual code that, say, communicates with a relational database is an Adapter. It translates the application interface into database SDK code.
I've been over this ground already, but to take an example from the sample code that accompanies Code That Fits in Your Head, here's a single method from the SqlReservationsRepository
Adapter:
public async Task Create(int restaurantId, Reservation reservation) { if (reservation is null) throw new ArgumentNullException(nameof(reservation)); using var conn = new SqlConnection(ConnectionString); using var cmd = new SqlCommand(createReservationSql, conn); cmd.Parameters.AddWithValue("@Id", reservation.Id); cmd.Parameters.AddWithValue("@RestaurantId", restaurantId); cmd.Parameters.AddWithValue("@At", reservation.At); cmd.Parameters.AddWithValue("@Name", reservation.Name.ToString()); cmd.Parameters.AddWithValue("@Email", reservation.Email.ToString()); cmd.Parameters.AddWithValue("@Quantity", reservation.Quantity); await conn.OpenAsync().ConfigureAwait(false); await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); }
This is one method of a class named SqlReservationsRepository
, which is an Adapter that makes ADO.NET code look like the application-specific IReservationsRepository
interface.
Such Adapters are often as 'thin' as possible. One dimension of measurement is to look at the cyclomatic complexity, where the ideal is 1, the lowest possible score. The code shown here has a complexity measure of 2 because of the null guard, which exists because of a static analysis rule.
In test parlance, we call such thin Adapters Humble Objects. Or, to paraphrase what Kris Jenkins said at the GOTO Copenhagen 2024 conference, separate code into parts that are
- hard to test, but easy to get right
- hard to get right, but easy to test.
You can do the same when sending email, querying a weather service, raising events on pub-sub infrastructure, getting the current date, etc. This isolates your application code from implementation details, such as particular database servers, SDKs, network protocols, and so on.
Shouldn't you be doing the same on the receiving end?
Fat Adapters #
In his article on Hexagonal Architecture Alistair Cockburn acknowledges a certain asymmetry. Some ports are activated by the application. Those are the ones already examined. An application reads from and writes to a database. An application sends emails. An application gets the current date.
Other ports, on the other hand, drive the application. According to Tomas Petricek's distinction between frameworks and libraries (that I also use), this kind of behaviour characterizes a framework. Examples include web frameworks such as ASP.NET, Express.js, Django, or UI frameworks like Angular, WPF, and so on.
While I usually do shield my Domain Model from framework code, I tend to write 'fat' Adapters. As far as I can tell, this is what some people have taken issue with.
Here's an example:
[HttpPost("restaurants/{restaurantId}/reservations")] public async Task<ActionResult> Post(int restaurantId, ReservationDto dto) { if (dto is null) throw new ArgumentNullException(nameof(dto)); Reservation? candidate1 = dto.TryParse(); dto.Id = Guid.NewGuid().ToString("N"); Reservation? candidate2 = dto.TryParse(); Reservation? reservation = candidate1 ?? candidate2; if (reservation is null) return new BadRequestResult(/* Describe the errors here */); var restaurant = await RestaurantDatabase.GetRestaurant(restaurantId) .ConfigureAwait(false); if (restaurant is null) return new NotFoundResult(); using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var reservations = await Repository .ReadReservations(restaurant.Id, reservation.At) .ConfigureAwait(false); var now = Clock.GetCurrentDateTime(); if (!restaurant.MaitreD.WillAccept(now, reservations, reservation)) return NoTables500InternalServerError(); await Repository.Create(restaurant.Id, reservation).ConfigureAwait(false); scope.Complete(); return Reservation201Created(restaurant.Id, reservation); }
This is (still) code originating from the example code base that accompanies Code That Fits in Your Head, although I've here used the variation from Coalescing DTOs. I've also inlined the TryCreate
helper method, so that the entire use-case flow is visible as a single method.
In a sense, we may consider this an Adapter, too. This Post
action method is part of a Controller class that handles incoming HTTP requests. It is, however, not that class that deals with the HTTP protocol. Neither does it parse the request body, or checks headers, etc. The ASP.NET framework takes care of that.
By following certain naming conventions, adorning the method with an [HttpPost]
attribute, and returning ActionResult
, this method plays by the rules of the ASP.NET framework. Even if it doesn't implement any particular interface, or inherits from some ASP.NET base class, it clearly 'adapts' to the ASP.NET framework.
It does that by attempting to parse and validate input, look up data in data sources, and in general checking preconditions before delegating work to the Domain Model - which happens in the call to MaitreD.WillAccept
.
This is where some people seem to get uncomfortable. If this is an Adapter, it's a 'fat' one. In this particular example, the cyclomatic complexity is 6. Not really a Humble Object.
Shouldn't there be some kind of 'use-case' model?
Use-case Model API #
I deliberately avoid 'use-case' model, 'mediators', or whatever other name people tend to use. I'll try to explain why by going through the exercise of actually extracting such a model. My point, in short, is that I find it not worth the effort.
In the following, I'll call such a model a 'use-case', since this is one piece of terminology that I tend to run into. You may also call it an 'Application Model' or something else.
The 'problem' that I apparently am meant to solve is that most of the code in the above Post
method is tightly coupled to ASP.NET. If we want to decouple this code, how do we go about it?
It's possible that my imagination fails me, but the best I can think of is some kind of 'use-case model' that models the 'make a reservation' use case. Perhaps we should name it MakeReservationUseCase
. Should it be some kind of Command object? It could be, but I think that this is awkward, because it also needs to communicate with various dependencies, such as RestaurantDatabase
, Repository
, and Clock
. A long-lived service object that can wrap around those dependencies seems like a better option, but then we need a method on that object.
public sealed class MakeReservationUseCase { // What to call this method? What to return? I hate this already. public object MakeReservation(/* What to receive here? */) { throw new NotImplementedException(); } }
What do we call such a method? Had this been a true Command object, the single parameterless method would be called Execute
, but since I'm planning to work with a stateless service, the method should take arguments. I played with options such as Try
, Do
, or Go
, so that you'd have MakeReservationUseCase.Try
and so on. Still, I thought this bordered on 'cute' or 'clever' code, and at the very least not particularly idiomatic C#. So I settled for MakeReservation
, but now we have MakeReservationUseCase.MakeReservation
, which is clearly redundant. I don't like the direction this design is going.
The next question is what parameters this method should take?
Considering the above Post
method, couldn't we pass the dto
object on to the use-case model? Technically, we could, but consider this: The ReservationDto
object's raison d'ĂȘtre is to support reception and transmission of JSON objects. As I've already covered in an earlier article series, serialization formats are inexplicably coupled to the boundaries of a system.
Imagine that we wanted to decompose the code base into smaller constituent projects. If so, the use-case model should be independent of the ASP.NET-based code. Does it seem reasonable, then, to define the use-case API in terms of a serialization format?
I don't think that's the right design. Perhaps, instead, we should 'explode' the Data Transfer Object (DTO) into its primitive constituents?
public object MakeReservation( int restaurantId, string? id, string? at, string? email, string? name, int quantity)
I'm not too happy about this, either. Six parameters is pushing it, and this is even only an example. What if you need to pass more data than that? What if you need to pass a collection? What if each element in that collection contains another collection?
Introduce Parameter Object, you say?
Given that this is the way we want to go (in this demonstration), this seems as though it's the only good option, but that means that we'd have to define another reservation object. Not the (JSON) DTO that arrives at the boundary. Not the Reservation
Domain Model, because the data has yet to be validated. A third reservation class. I don't even know what to call such a class...
So I'll leave those six parameters as above, while pointing out that no matter what we do, there seems to be a problem.
Return type woes #
What should the MakeReservation
method return?
The code in the above Post
method returns various ActionResult
objects that indicate success or various failures. This isn't an option if we want to decouple MakeReservationUseCase
from ASP.NET. How may we instead communicate one of four results?
Many object-oriented programmers might suggest throwing custom exceptions, and that's a real option. If nothing else, it'd be idiomatic in a language like C#. This would enable us to declare the return type as Reservation
, but we would also have to define three custom exception types.
There are some advantages to such a design, but it effectively boils down to using exceptions for flow control.
Is there a way to model heterogeneous, mutually exclusive values? Another object-oriented stable is to introduce a type hierarchy. You could have four different classes that implement the same interface, or inherit from the same base class. If we go in this direction, then what behaviour should we define for this type? What do all four objects have in common? The only thing that they have in common is that we need to convert them to ActionResult
.
We can't, however, have a method like ToActionResult()
that converts the object to ActionResult
, because that would couple the API to ASP.NET.
You could, of course, use downcasts to check the type of the return value, but if you do that, you might as well leave the method as shown above. If you plan on dynamic type checks and casts, the only base class you need is object
.
Visitor #
If only there was a way to return heterogeneous, mutually exclusive data structures. If only C# had sum types...
Fortunately, while C# doesn't have sum types, it is possible to achieve the same goal. Use a Visitor as a sum type.
You could start with a type like this:
public sealed class MakeReservationResult { public T Accept<T>(IMakeReservationVisitor<T> visitor) { // Implementation to follow... } }
As usual with the Visitor design pattern, you'll have to inspect the Visitor interface to learn about the alternatives that it supports:
public interface IMakeReservationVisitor<T> { T Success(Reservation reservation); T InvalidInput(string message); T NoSuchRestaurant(); T NoTablesAvailable(); }
This enables us to communicate that there's exactly four possible outcomes in a way that doesn't depend on ASP.NET.
The 'only' remaining work on the MakeReservationResult
class is to implement the Accept
method. Are you ready? Okay, here we go:
public sealed class MakeReservationResult { private readonly IMakeReservationResult imp; private MakeReservationResult(IMakeReservationResult imp) { this.imp = imp; } public static MakeReservationResult Success(Reservation reservation) { return new MakeReservationResult(new SuccessResult(reservation)); } public static MakeReservationResult InvalidInput(string message) { return new MakeReservationResult(new InvalidInputResult(message)); } public static MakeReservationResult NoSuchRestaurant() { return new MakeReservationResult(new NoSuchRestaurantResult()); } public static MakeReservationResult NoTablesAvailable() { return new MakeReservationResult(new NoTablesAvailableResult()); } public T Accept<T>(IMakeReservationVisitor<T> visitor) { return this.imp.Accept(visitor); } private interface IMakeReservationResult { T Accept<T>(IMakeReservationVisitor<T> visitor); } private sealed class SuccessResult : IMakeReservationResult { private readonly Reservation reservation; public SuccessResult(Reservation reservation) { this.reservation = reservation; } public T Accept<T>(IMakeReservationVisitor<T> visitor) { return visitor.Success(reservation); } } private sealed class InvalidInputResult : IMakeReservationResult { private readonly string message; public InvalidInputResult(string message) { this.message = message; } public T Accept<T>(IMakeReservationVisitor<T> visitor) { return visitor.InvalidInput(message); } } private sealed class NoSuchRestaurantResult : IMakeReservationResult { public T Accept<T>(IMakeReservationVisitor<T> visitor) { return visitor.NoSuchRestaurant(); } } private sealed class NoTablesAvailableResult : IMakeReservationResult { public T Accept<T>(IMakeReservationVisitor<T> visitor) { return visitor.NoTablesAvailable(); } } }
That's a lot of boilerplate code, but it's so automatable that there are programming languages that can do this for you. On .NET, it's called F#, and all of that would be a single line of code.
Use Case implementation #
Implementing MakeReservation
is now easy, since it mostly involves moving code from the Controller to the MakeReservationUseCase
class, and changing it so that it returns the appropriate MakeReservationResult
objects instead of ActionResult
objects.
public sealed class MakeReservationUseCase { public MakeReservationUseCase( IClock clock, IRestaurantDatabase restaurantDatabase, IReservationsRepository repository) { Clock = clock; RestaurantDatabase = restaurantDatabase; Repository = repository; } public IClock Clock { get; } public IRestaurantDatabase RestaurantDatabase { get; } public IReservationsRepository Repository { get; } public async Task<MakeReservationResult> MakeReservation( int restaurantId, string? id, string? at, string? email, string? name, int quantity) { if (!Guid.TryParse(id, out var rid)) rid = Guid.NewGuid(); if (!DateTime.TryParse(at, out var rat)) return MakeReservationResult.InvalidInput("Invalid date."); if (email is null) return MakeReservationResult.InvalidInput("Invalid email."); if (quantity < 1) return MakeReservationResult.InvalidInput("Invalid quantity."); var reservation = new Reservation( rid, rat, new Email(email), new Name(name ?? ""), quantity); var restaurant = await RestaurantDatabase.GetRestaurant(restaurantId).ConfigureAwait(false); if (restaurant is null) return MakeReservationResult.NoSuchRestaurant(); using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var reservations = await Repository.ReadReservations(restaurant.Id, reservation.At) .ConfigureAwait(false); var now = Clock.GetCurrentDateTime(); if (!restaurant.MaitreD.WillAccept(now, reservations, reservation)) return MakeReservationResult.NoTablesAvailable(); await Repository.Create(restaurant.Id, reservation).ConfigureAwait(false); scope.Complete(); return MakeReservationResult.Success(reservation); } }
I had to re-implement input validation, because the TryParse
method is defined on ReservationDto
, and the Use-case Model shouldn't be coupled to that class. Still, you could argue that if I'd immediately implemented the use-case architecture, I would never had had the parser defined on the DTO.
Decoupled Controller #
The Controller method may now delegate implementation to a MakeReservationUseCase
object:
[HttpPost("restaurants/{restaurantId}/reservations")] public async Task<ActionResult> Post(int restaurantId, ReservationDto dto) { if (dto is null) throw new ArgumentNullException(nameof(dto)); var result = await makeReservationUseCase.MakeReservation( restaurantId, dto.Id, dto.At, dto.Email, dto.Name, dto.Quantity).ConfigureAwait(false); return result.Accept(new PostReservationVisitor(restaurantId)); }
While that looks nice and slim, it's not all, because you also need to define the PostReservationVisitor
class:
private class PostReservationVisitor : IMakeReservationVisitor<ActionResult> { private readonly int restaurantId; public PostReservationVisitor(int restaurantId) { this.restaurantId = restaurantId; } public ActionResult Success(Reservation reservation) { return Reservation201Created(restaurantId, reservation); } public ActionResult InvalidInput(string message) { return new BadRequestObjectResult(message); } public ActionResult NoSuchRestaurant() { return new NotFoundResult(); } public ActionResult NoTablesAvailable() { return NoTables500InternalServerError(); } }
Notice that this implementation has to receive the restaurantId
value through its constructor, since this piece of data isn't part of the IMakeReservationVisitor
API. If only we could have handled all that pattern matching with a simple closure...
Well, you can. You could have used Church encoding instead of the Visitor pattern, but many programmers find that less idiomatic, or not sufficiently object-oriented.
Be that as it may, the Controller now plays the role of an Adapter between the ASP.NET framework and the framework-neutral Use-case Model. Is it all worth it?
Reflection #
Where does that put us? It's certainly possible to decouple the Use-case Model from the specific framework, but at what cost?
In this example, I had to introduce two new classes and one interface, as well as four private
implementation classes and a private
interface.
And that was to support just one use case. If I want to implement a query (HTTP GET
), I would need to go through similar motions, but with slight variations. And again for updates (HTTP PUT
). And again for deletes. And again for the next resource, such as the restaurant calendar, daily schedule, management of tenants, and so on.
The cost seems rather substantial to me. Do the benefits outweigh them? What are the benefits?
Well, you now have a technology-neutral application model. You could, conceivably, tear out ASP.NET and replace it with, oh... ServiceStack. Perhaps. Theoretically. I haven't tried.
This strikes me as an argument similar to insisting that hiding database access behind an interface enables us to replace SQL Server with a document database. That rarely happens, and is not really why we do that.
So to be fair, decoupling also protects us from changes in libraries and frameworks. It makes it easier to modify one part of the system without having to worry (too much) about other parts. It makes it easier to subject subsystems to automated testing.
Does the above refactoring improve testability? Not really. MakeReservationUseCase
may be testable, but so was the original Controller. The entire code base for Code That Fits in Your Head was developed with test-driven development (TDD). The Controllers are mostly covered by self-hosted integration tests, and bolstered with a few unit tests that directly call their action methods.
Another argument for a decoupled Use-case Model is that it might enable you to transplant the entire application to a new context. Since it doesn't depend on ASP.NET, you could reuse it in an Android or iPhone app. Or a batch job. Or an AI-assisted chat bot. Right? Couldn't you?
I'd be surprised if that were the case. Every application type has its own style of user interaction, and they tend to be incompatible. The user-interface flow of a web application is fundamentally different from a rich native app.
In short, I consider the notion of a technology-neutral Use-case Model to be a distraction. That's why I usually don't bother.
Conclusion #
I usually implement Controllers, message handlers, application entry points, and so on as fairly 'fat Adapters' over particular frameworks. I do that because I expect the particular application or user interaction to be intimately tied to that kind of application. This doesn't mean that I just throw everything in there.
Fat Adapters should still be covered by automated tests. They should still show appropriate decoupling. Usually, I treat each as an Impureim Sandwich. All impure actions happen in the Fat Adapter, and everything else is done by a pure function. Granted, however, this kind of architecture comes much more natural when you are working in a programming language that supports it.
C# doesn't really, but you can make it work. And that work, contrary to modelling use cases as classes, is, in my experience, well worth the effort.