Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
Want to build better APIs that can evolve over time as your system requires changes? Over time, your API is going to change. It just will evolve, and you don’t want to handcuff yourself. Here are five tips that will help you change your API design over time without creating breaking changes.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
Where do you generate identifiers? When I say identifier (aka ID), most would be thinking about a database and primary key. It’s typical to let your database generate the ID, and often, this is an auto-incrementing value.
Depending on your context, if you’re in a distributed environment and at a large scale, you might not want to be using auto-incrementing IDs because you could be generating the same ID on different database instances. Ultimately, that could pose a large problem if you ever need to merge data between database instances. Again, this is very context-specific if you’re in a large distributed environment.
However, that’s not the only issue with auto-incrementing IDs. More specifically, the issue is letting your database generate the ID for you, and you want to perform work asynchronously.
For example, if a user places an Order, we may place a message on a queue to accept the Order and process it asynchronously. We need to generate the ID farther up the call static. We can do that instead at our API by generating a UUID and returning that to the client. We could place a PlaceOrder message with the same OrderID in our queue.
Now, asynchronously, our Worker process would pick that message up off the Queue, and when it processes the message, it would persist our Order to our database with the UUID defined by the API in the message.
Incrementing integers or UUIDs are often meaningless when you’re looking at them. They need more context to make sense of what they relate to. Meaningful identifiers, however, represent that context.
Let’s say you see an order with the ID of ddfa1987-0d7f-4afc-8a0b-8c104dd9956d
That’s not very helpful. However, let’s say the ID was CA-ON-240117-21.
CA represents the country code, ON represents the state/province, and 240117 might be the date, 21 might be the order that day. That might give you meaningful context in your system when you’re looking at the order without having to dig up a lot of information. How you define these meaningful identifiers is up to your context and what information you often need.
An identifier identifies something.
I don’t necessarily mean this has to be the primary key of a database record. That can still be a UUID, but how you represent that to an end-user or expose it in your API can be meaningful and unique. You may choose to persist that or even compose that data at runtime when needed.
As always, context is king in your API design.
Provide clients of your API response with information about what they can do based on the current state.
Here’s some order data returned from an API.
What can we do with this order in this state? Sure, we, as developers, read the documentation and see all the different types of functionality exposed. Let’s say we can cancel an order. But when can we cancel an order? Clearly, we can’t cancel a completed order. The documentation might say, but we’d need to know that at design time. What if the list of statuses changes or the rules on the backend around when the order can be canceled?
We can evolve by providing the actions to our clients in our responses so they can decide at runtime and not design time.
Now, we’re providing our clients with the actions they can perform based on the state of the order. If we change our rules about when you can cancel an order, our client doesn’t need to know this and rewrite the code about when it can provide the user some UI to cancel the order; we’re telling it when throughout actions. Our client code can now be reactive to the responses and show the user the appropriate UI when certain actions are returned in our responses.
You’ll notice if you’re doing this, you’re not constructing URIs in your client code since the server is providing it to you. URIs now become opaque to your clients.
Another tip is don’t handcuff yourself in your response so you can’t change it. Allow yourself room to make changes. A simple example of this is if you return an array/collection.
Because you’ve defined an array as the root of your response, you can’t add anything else but a list of orders.
Allow yourself room to evolve and add to your responses in your API design.
Developers are technical people, but that doesn’t mean you should expose technical nonsense jargon in your API that has nothing to do with your API’s domain.
Use the language of the domain you’re in. Some things will be CRUD (create, read, update, delete), but more often than not, in the core of your domain, people will not use CRUD terms at all, and they will be very specific and use domain-specific concepts.
You’re not updating an order, so you can change the status to canceled. You are exposing the Cancel Order functionality, which likely has a lot more to it than updating a column/property in your database.
Use the terminology of the domain and expose that in your API. If someone is consuming your API and they are familiar with the domain, it should seem natural to them. If they are unfamiliar with the domain, they’ll likely learn a lot about it and some of the workflows and processes based on all the terminology because of your API design.
That may seem obvious, but what’s less common is actually using the terminology about the actual actions taken. Sure, people use domain terminology for entities based on nouns, but what’s less common is exposing the actual behaviors and going beyond create/read/update. Be explicit about the behaviors you’re exposing.
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.