Rethinking API Versioning with Domain-Driven Design
How to manage breaking changes elegantly and get rid of version IDs for good by being nice to your clients.
Change is inevitable. According to the Agile manifesto, we should welcome change. Is this also true for your API?
Dealing with changes in APIs can be hard and there is no bullet-proof way of managing it. Whether you use URL path, parameter, or HTTP header versioning — all cause pretty much the same pain — the change still needs to be communicated back to the consumers. There is no technical solution for that. It is really a people problem.
Another difficulty is the need to maintain multiple versions potentially forever.
Is such a change really welcomed by developers? I doubt it.
There are already attempts to make things better, like HATEOAS, but most of them are too impractical and/or difficult to develop.
Although a shiny new approach by GraphQL has been gaining popularity in the last few years, I believe that the simple plain REST will never die out.
In this text, we will concentrate on endpoints such as:
GET /product/123 { "name": "My Product", "description": "Lorem ipsum...", "price": 14.5 }
What Is Wrong with APIs
Most Restful traditionally built APIs align a resource with a data entity.
In the system that provides the /product
resource from the example above, one can surely find a Restful controller ProductController
, a data-transfer object ProductDto
, a domain object Product
, and of course, a database table PRODUCTS
.
This entity-driven system design practically means just exposing naked data to the client. In the worst case, even the raw database structure just as it is.
This is unfortunate because data is usually very volatile. By exposing raw data to the client you basically let implementation details leak into the API.
If the API is coupled to volatile details, the system becomes hard to change. Changes that are difficult to carry out are usually not very welcome.
Instead of the data-driven approach, your API design should be behavior-driven. Don’t think about resources as mere data structures with particular CRUD operations. Rather, think about resources as the behavior of the system within particular use-cases.
A rule of thumb would be the number of operations the client has to invoke to achieve a single goal being not greater than one.
As Gregor Hohpe states in his Cloud Strategy: “Don't build elaborate APIs that mimic the back-end system's design. Instead, build consumer-driven APIs that provide the service in a format that the front-ends prefer.”
This is a piece of good advice. Not following it often leads to another big problem: accidental coupling between use-cases. Let me show you some example:
GET /product/123 { "name": "My Product", "description": "Lorem ipsum...", "price": 14.5, "availability": "soldout", "delivery": ["PPL", "FedEx"] }
In the data-driven API design, the producer must make assumptions about what to do in order to satisfy all consumers at once. In this particular case, the attributes name
, description
, and price
are required by the Catalog page, while availability
and delivery
are needed only by the Checkout page.
This approach not only forces clients to consume unneeded data and unnecessarily increase traffic. It also couples use-cases tightly to each other. Every change performed in order to satisfy the needs of one consumer will somehow impact all the others. If a consumer is not liberal enough any minor unintentional change can break it.
Postel’s law of conservative producers and liberal consumers also has its trade-offs. It makes the system more robust, but less precise and more complex (it breaks the separation of concerns).
We can resolve the problem by adding a domain context (remember microservices?):
GET /catalog/product/123 { "name": "My Product", "description": "Lorem ipsum...", "price": 14.5 } GET /checkout/product/123 { "availability": "soldout", "delivery": ["PPL", "FedEx"] }
Now, as both use-cases are decoupled, each resource can be treated individually.
It is still wise to make changes with caution as existing users often use the system beyond its original design.
Evolutionary API Versioning
Roy Fielding, the author of REST, already gave us an answer to the question of how we can version our APIs: “Don’t.”
He explains: “Versioning is used in ways that are informative rather than contractual.” And further: “REST is designed primarily to improve evolvability, the ability to change over time without starting over.”
This makes sense, but the devil is always in the detail. How can we make our API evolutionary?
Applying the Domain-driven design would be the first step. Moving away from data-centric APIs would mean considering endpoints such as:
GET /GetAllProducts GET /GetProductDetail/123 GET /SearchProducts?name=... ...
Say what you do, do what you say.
How can it help us with managing breaking changes? When we are honest about our endpoints we can make the change explicit.
Well, there is always some business motivation behind every change. If we communicate this motivation to the clients, they will gain better understanding of why the change was done and what it will bring them if they switch.
GET /GetProductsAtDiscount GET /GetPromoProducts ...
Understanding the benefits will make the clients want to switch. Consider these two possibilities:
/v1/ponies => /v2/ponies
or
/ponies => /unicorns
Which one would be most tempting for users to switch to? I guess you get the point 🦄
Now, you can finally get rid of version IDs for good. Being nice to your clients pays off!
One more thing. Breaking changes are sometimes inevitable; however, it should be a last resort.
If you have to introduce breaking changes every time you release a new feature then you probably have a bigger problem to fix: wrong abstraction.
A good API reflects its purpose well. Purpose tends to be stable and as such it is the best foundation for designing stable APIs.
Summary
There is no simple technical solution for API versioning. Instead of looking for one, we can apply the same principles of well-structured, decoupled, and maintainable software architectures:
- A good API reflects its purpose.
- Apply the Domain-driven design to your APIs.
- Say what you do, do what you say.
- Make the change explicit.
- Don’t break your API, evolve it.
Happy resting!