Adopting New API Versions With Hexagonal Architecture | by Andrew O’Hara | Mar, 2022

Stay up-to-date without breaking things

Image by Steve Johnson on Unsplash

When you’ve been comfortable working with your old API for a long time, it can be onerous to adopt a new API version; particularly if you never designed for it. In this guide, I’ll show you a method to adopt even drastically new iterations, with minimal churn to your code.

Let’s say that we’re a vet clinic, and we have an app that gets some data from our API, formats it as HTML, and then prints it to the console. You can extrapolate how we might build a fully functional web or mobile app from this outline.

When we designed this app, we had a very specific idea for how it would look, so we designed our model and API around those requirements. It works perfectly for us, and we’re very happy with it.

But now we have a new requirement: we have to show the cat’s favorite food. The backend team already supports this, but it’s only available in their new API version. Lets have a look at the new schema they’ve sent us:

Turns out they’ve made a lot of changes to the new schema, and there are several issues:

  • we don’t want to support the new values ​​of breedand they use CamelCase instead of the snake_case we want.
  • colours is a set of enums instead of the grey and brown booleans we want
  • id and ownerId are UUIDs instead of the integers we want
  • appointments are missing from the cat schema; They’re only available from a completely different API operation

Imagine for a moment that our app is massive: if we were to adopt this new schema as our model, the changes would be huge, and would require extensive regression testing. We can’t justify that amount of effort when all we wanted was for a new property to be added.

If we adopt some of the principals of Hexagonal Architecture (specifically Ports and Adapters), we can insulate our app from most of these issues. We can adopt the new API version, add the new feature, and change very little of our existing code. All of this is possible with an adapter the converts the new schema to the model we’re accustomed to.

You can design many different adapters to fit a single port

Port

In software terms, a port is the interface that defines the contract we want to have fulfilled.

fun interface Horn {
fun honk()
}

Adapter

An adapter is the implementation of the port’s interface. There can be several versions that can be swapped in and out as needed, and even on the fly, or layered with functional programming.

class CarHorn: Horn {
override fun honk() {
println("Beep")
}
}
class TruckHorn: Horn {
override fun honk() {
println("BURRRRRRRRRP")
}
}

Define our Port

So, how do we use Ports and Adapters to help us? If our goal is to use the same Cat model as before, then all the port really needs to do is get a Cat by its id. But first we need to reconcile the difference between the old integer id and the new UUID. Thankfully, the two can be easily expressed as a String , so our port will use that. We can afford a small compromise by updating our Cat model to use a String id.

Implement the v1 Adapter

Now, lets come up with a v1 adapter to implement our port. It’s very simple, because all it does is delegate the task to the ClientV1.

Implement the v2 Adapter

Next we need to create an adapter forClientV2. This one is a bit more complex because it needs to make two API calls and merge the result into the original Cat model. We’ll also take this opportunity to get the new favouriteFood field from the CatDtoV2.

In order to actually use this new CatsDao port, we must update our CatUi to use it instead of the ClientV1 . Then we must update our main method to inject an adapter. Then we can safely render the cat from either API, and the favorite food, if present.

So now we’ve successfully adopted a completely new UI with minimal churn. But why did we maintain a V1 adapter?

Reason 1: Safety

It’s often valuable to still support the old API and use a feature flag to slowly roll out the update or quickly roll back in case of an issue.

Reason 2: Legacy Data

If the v2 API doesn’t have access to all of the old data, then we’ll need a way to fall back to v1 .

To work around these concerns, we now have 2 new adapters. The toggled adapter uses a feature flag to determine whether we delegate to the v1 or backCompat adapter, and backCompat will attempt to get the Cat from v2 falling back to v1 if not found.

Our original app had a problem, and the improvements so far have not changed that. After successfully releasing support for favouriteFoodwe now want to support all the new colors available in the v2 schema. The Cat model doesn’t support this, but updating it would complicate our ClientV1.

Before, this would have been a huge problem. But now that we have the tools to insulate our internal model from new API versions, we can take it a step further and insulate it from the (now) legacy v1 schema by making the ClientV1 return a new CatDtoV1and updating the adapter to convert from CatDtoV1to a Cat. Once that’s done, it will be much safer to support this new feature, while still supporting both API versions.

Don’t forget to make the corresponding update to the v2 adapter.

With this guide, you should be able to adopt new API versions with minimal churn to your existing code. All it requires is to replace your direct dependency on the client with a port, implemented by an adapter that converts from the new model to one you’re comfortable with. Please let me know in the comments if this helps you tackle an onerous migration. Good luck!

For the full source code, see the repo below:

Leave a Comment