Ports and Adapters on a legacy code base

Ben Freshwater (he/him)
Cazoo Technology Blog
4 min readMar 24, 2022

Intro

When I joined Cazoo, we had a code base which was in need of some attention. A lot of business logic was intertwined with intimate details of external dependencies that it did not need to know about. Many of us had experience with ports and adapters on greenfield code bases and projects where the pattern was already established. We knew it would be a bigger challenge to introduce it to legacy code.

In this article, I will go through the journey we took to improve our code with Ports and Adapters, starting with the work we did together as a team to understand what we would move to the new architecture.

This article assumes some background knowledge of Ports and Adapters. If you’re unfamiliar with the pattern, take a look at this article by Tugce Konuklar for an introduction to the architecture.

Understanding the current system

We set out with a plan to refactor one endpoint in our system, decouple dependencies from business logic, encapsulate the dependencies into adapters and use them in the business logic as abstracted ports. Before diving straight in to this, we needed to understand what was business logic and what was not.

Given this was in the midst of the 2020 COVID pandemic we went to our trusty friend Miro, an online whiteboarding space with lots of great tools to assist collaboration. Here we asked ourselves;

  • what does the endpoint achieve?
  • what does it do to achieve this?

Everyone would collaborate to describe line-by-line what the endpoint does in code order to achieve its goal. And we would write them on a post-it. We got something a bit like this:

A rough outline of what the domain does. It imports the document client, gets customer application from state, fetches bearer token, error if already sent to checkout, build request for checkout, return to client.
A rough outline of what the domain does.

The domain is doing a number of things which are details of how to achieve something. Such as importing document client to then get customer application from state. And fetching bearer tokens. This information isn’t necessary to achieve the goal of the domain, and can be abstracted so the code only knows what it needs to do, the details of how it does this can be encapsulated into a class or function that implements the abstraction.

From this, we started to see what was business logic and what was not. We could see that Dynamo was a clear implementation of a repository port. Actions such as logging and checkout-specific credentials could also be introduced in to a port.

Here’s another view of the same steps with the non-core ones marked as adapters.

The same domain with ports and adapters identified. Get customer application from state is a port, dynamo get is an adapter, error if already sent to checkout is a use case action, return to client is a port, checkout resumption service is an adapter.
Purple, port; orange, domain; green, adapter

How do you know what is business logic and What is not?

I think this is easier to answer if you change the question: How do you know if something is an adapter? There are a couple of properties which can help to answer this:

  • Does the code interact with an external system?
  • Does the code retrieve something to be used later on?
  • Does the code have a different responsibility to the rest of the code in the same procedure?

If the answer is yes to any one of the above, it is a good indication that the code can be encapsulated. This should help encapsulate the big pieces of code which are not related to the business logic.

We were now ready to encapsulate these concepts in to ports and adapters. Quite simply, we took the post-its which we thought were external concerns and had a discussion. We then tried to abstract the concepts. Dynamo is a NoSQL database, but what service does it provide to our endpoint? For us, it acts as a store for application state. We felt that ‘repository’ was a good noun to describe the abstraction of data storage.

With the ports and adapters identified. It helps us understand what our core domain is going to do. For the most part, it performs business logic actions such as validating that an application can be accepted based on business rules, and acts as the glue which connects the repository/logging/checkout abstractions together.

The domain layer becomes an abstracted view of the business logic. It does not need to know how it will store to a repository, or how to notify the rest of the business that something happened, it just needs to know that it has to do it.

Summary

Ports and adapters is a great architecture pattern to encapsulate details of our integrations and abstract them away from our business logic. It can help make the core logic of your code much clearer. This first post should help in gaining an understanding of how to think with Ports and Adapters.

In future posts, we will go through the details of what the core domain looks like in code. But as an introduction, I wanted to highlight the ways in which you can collaboratively identify the different responsibilities of your code. In future articles, we will dive deeper in to how to build out your ports, adapters, use cases, and supporting layers.

--

--