Revisiting Essential Elements of the Ports and Adapters Architecture Style

I sometimes see needless complexity and confusion in the name of clean architecture/ports and adapters style. People either create needless abstractions or create/use complicated “frameworks” to implement what is essentially a fairly simple idea and can be achieved out of the box in most programming languages natively. I have made that mistake in the past and I have written about this architectural style before as well, but I figured I will attempt to define each element of the style a bit and also show some C# code examples to illustrate the points (non-production grade code).

What are Ports?

Ports are interfaces designed around what the domain needs from the world, so to speak, in order to carry out some business function. In this architectural style there are two kinds:

Incoming Ports

Ports invoked from outside of the application to start a business meaningful conversation with the application e.g. a command or a query coming from outside:

public interface IOpenPurchaseOrders
{
void OpenNewPurchaseOrder(PurchaseOrderRequest input);
}
view raw UseCasePort.cs hosted with ❤ by GitHub

Use Cases therefore would also be considered as incoming ports because they are technology agnostic and can be invoked from a myriad of technology specific ways.

Use cases are also invoked by tests via a test adapter. That’s what allows a Ports and Adapters implementation to be driven by tests all the same as an HTTP request, or a message off a queue or a CRON trigger.

Outgoing Ports

Use cases then typically invoke one or more outgoing ports to continue and complete the meaningful conversation. E.g. getting or putting some data in the database, publishing messages to a queue etc:

public interface IPurchaseOrderRepository
{
void Create(PurchaseOrder purchaseOrder);
void Update(PurchaseOrder purchaseOrder);
PurchaseOrder GetById(PurchaseOrderId purchaseOrderId);
PurchaseOrder[] GetByOpeningDateRange(
DateTime searchStartDate, DateTime searchEndDate);
}
public interface IPublishPurchaseOrderEvents
{
void Publish(PurchaseOrderEvent[] purchaseOrderEvents);
//...more publishing APIs
}

Putting everything together, the full interaction between incoming ports (the use case) and outgoing ports (databases and external devices), might look like this:

public class OpenPurchaseOrderUseCase : IOpenPurchaseOrders
{
public OpenPurchaseOrderUseCase(
IPurchaseOrderRepository purchaseOrderRepository,
IPublishPurchaseOrderEvents purchaseOrderEventPublisher)
{
this.purchaseOrderRepository = purchaseOrderRepository;
this.purchaseOrderEventPublisher =
purchaseOrderEventPublisher;
}
void OpenNewPurchaseOrder(PurchaseOrderRequest input)
{
// other code excluded for brevity
var newPurchaseOrder = Process(input);
purchaseOrderRepository.Create(newPurchaseOrder);
purchaseOrderEventPublisher.Publish(newPurchaseOrder.Events);
}
}

The protocol for a port is given by the purpose of the conversation between the two devices.

Alistair Cockburn (Hexagonal Architecture)

What are Adapters?

Adapters are technology specific implementations for these port interfaces.

public sealed class KafkaPublisher : IPublishPurchaseOrderEvents
{
void Publish(NewPurchaseOrderOpened newPurchaseOrderOpened)
{
// Kafka specific publishing code.
}
}
public sealed class OraclePurchaseOrderRepository : IPurchaseOrderRepository
{
void Create(PurchaseOrder purchaseOrder)
{
// Oracle specific data access code
}
void Update(PurchaseOrder purchaseOrder)
{
// Oracle specific data access code
}
// .. other methods
}

Some adapters need not be written per se e.g. HTTP adapters come in the form of ASP.NET Core controller actions that do a lot of heavy lifting of converting raw data from outside the hexagon into something that target use case ports need.

Test adapters likewise are just tests using whatever testing framework you use:

[Fact]
public void ShouldCreatePurchaseOrderWithExpectedInformation()
{
var purchaseOrderRepositorySpy = new PurchaseOrderRepositorySpy();
var publisherDummy = new DummyPublisher();
var useCase = new OpenPurchaseOrderUseCase(
purchaseOrderRepositorySpy,
publisherDummy);
var purchaseOrderRequest = CreateValidPurchaseOrderRequest();
var expectedPurchaseOrder = CreateFrom(purchaseOrderRequest);
useCase.OpenNewPurchaseOrder(purchaseOrderRequest);
purchaseOrderRepositorySpy.Items.Contains(expectedPurchaseOrder);
}
view raw Test.cs hosted with ❤ by GitHub

All these implementations typically get wired up at the application bootstrapping level using Dependency Injection and that’s what makes various components of this architecture loosely coupled. At compile time the use cases don’t know anything about SQL databases, Kafka broker or test, that’s only resolved at runtime.

Essential structure of a Ports and Adapters system

At its core, this is all there is to Ports and Adapters architectural style (aka Hexagonal architecture). There can certainly be more internal sub-structure than this, it depends on the complexity of the problem/solution domain. But in Alistair Cockburn’s words, “The framework is there, I am done and I can walk away.”

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.