Writing Maintainable Code Tips: API Models != Domain Models
Using API models as your domain models in your business logic is not a good idea and leads to hard-to-maintain systems.
I think this is one of the problems that I've had to face most frequently throughout my career. What is, a priori, a trivial decision ends up becoming a thorn in your side much later in the development process. You might wonder what I'm actually talking about... well I guess the title probably gave it away.
TL;DR: decouple your API models from your business logic/domain ones using a translation layer in the API code. Let your system be internally independent from what is required from the outside.
(Almost) Every software will have some sort of interface with the outside world. And if you are a backend developer like me, that will mean some sort of API and not a fancy UI.
These APIs come in different styles and shapes, but at some point in your system you will find yourself in a method or function with some structures that represent the data flowing into your system.
From that point onwards the business-logic of the system takes over. The same applies when you need to send information generated by the business logic to outside entities.
A common practice that I've found almost everywhere is to forward the objects received from the API into the business logic layer.
The reasoning behind this is straight-forward: you have the business logic built to serve the API and the objects received through it are entities that map exactly to what you use in the business logic. Why wouldn't you use them?
There are a couple of flaws in this reasoning. Let's start with the most critical one: it strongly couples your business logic to your interface.
What do I mean by that? Well... now they cannot evolve separately. If you ever need to augment the object with some other information you cannot do it without modifying the software's external interface. If that is something completely internal like an implementation detail, bad luck, you are stuck with what you get.
Generally you will end up using the same concepts throught many places in your business logic and the entities might not come exclusively through the same API.
Decoupled code is the key for writing maintainable and testable software. The least coupled your code is, the easier it is to maintain, test and evolve.
An example is a GRPC endpoint that retrieves an entity stored in a database. The same thing goes through two completely different APIs: the database's and GRPC response object. I've seen this solved by using the protobuf model in the repo layer that reads and writes from and to the database!
At this point your whole system is fully coupled and evolving your system will be painful because any change in the interface will either require a change in the database layer or the repo will be returning incomplete objects. Also, a change in the database format will impact the interface of your system.
All changes now need to be end-2-end and cannot be isolated.
Another reason is to reduce unintended effects when introducing code changes.
This happens frequently in languages that have automatic mappers between domain objects and schema-less API schemes, for example Java + Spring + REST APIs. It is common to just put the domain object as part of the response and let the framework transform it for you.
Some time after that you decided that an extra field would be useful in the model to hold some implementation-relevant information. If you forget that it was being used in an API, then without realizing it you are now exposing your new field to all the API clients.
Your code changes were nowhere near the API but you still managed to accidentaly change it; just hope it was not a sensitive field if that ever happens.
What's the correct approach in my opinion? Have separate models for your API and your business logic, then apply a translation layer between one and the other in the API implementation logic (Controllers).
It will be a little bit more effort at the beginning and it will appear to be useless as the objects are the same. But remember, that only happens when you first write the system and will likely not hold true once it starts evolving.
If you let it slide and the practice becomes entrenched, I've found it to be almost impossible to fix after-the-fact.
The cost of decoupling is really high once a class has permeated through several layers of the business logic, large sections of the code need to be modified for "little" in reward. Doing it is boring, dangerous and hard to sell it to other stakeholders.
So, be proactive and don't let it happen to your system
Comments powered by Talkyard.