It's not a contract if you can't enforce it
It just so happened that lately I have been hearing quite a bit about “contracts” and how good they are at keeping modules decoupled. The argument goes something like this:
Martin: This module has too many responsibilities. Let’s split it into two. These are different responsibilities anyway. Lars: Well, they require the same data. Martin: The data can be passed from one to the other in Data Transfer Objects. Lars: If we do this, instead of one cohesive module, we will have two which are tightly coupled to each other. Martin: Coupled? No, they will be decoupled. They will be separated, autonomous, different teams will be able to work on each one in parallel. What do you mean, “coupled”? Lars: If you need to change one, you will most likely need to change the other one as well. Martin: It won’t happen, we will introduce a contract between them! The DTO will define the contract.
Let’s stop and think about this for a moment. If we merely declare something that tends to change a “contract”, we will get a “contract” that tends to change, and so not really a contract at all.
Example: simple web application
Let’s look at a very simple example. Say we are writing a simple financial application, and we want to display our customers’ accounts in some UI. The naive interpretation of Separation of Concerns tells us we need to cleanly separate presentation layer from application layer. Can we decouple them by declaring a contract? Let’s make the application layer first. We need some kind of account service (this is just to illustrate the point. Don’t do this in real projects).
public final class AccountService {
public Account getAccount(String id) {
// Find Account by ID.
}
}
We also need an account entity, which is created somewhere in lower layers from the data fetched from database.
public final class Account {
private final String id;
private final BigDecimal balance;
// Constructor, getters, some behaviour...
}
We interpret the Separation of Concerns principle such that domain/application layer objects cannot know anything about even the existence of a presentation layer, and therefore cannot present themselves, not even in some abstract way. Therefore we need the presentation controller to extract data from them and store it in a presentation model. This is where we define the “contract”.
public final class AccountDto {
private final String id;
private final BigDecimal balance;
// Constructor, getters...
}
Now we can display the account data. We need to convert the data from domain layer to presentation layer.
public final class Controller {
public String accountView(String id, Model model) {
model.addAttribute(
"account",
fromEntity(accountService.getAccount(id))
);
return "account";
}
private static AccountDto fromEntity(Account entity) {
return new AccountDto(
entity.getId(),
entity.getBalance()
);
}
// Some wiring, etc.
}
And now we can display it.
<html>
<body>
<p>
Account ID: ${account.getId()}
Balance: ${account.getBalance()}
</p>
</body>
</html>
Decoupled?
So there we have it. According to this argument, we have decoupled our application layer (AccountService
and Account
) from our presentation layer (the HTML template). They don’t depend on each other, all they depend on is the contract between them - the AccountDto
. As long as the DTO doesn’t change, we can develop these layers completely independently. We can change the colour of the screen background, we can switch account ID and balance such that balance is at the top and ID is at the bottom, we can change their sizes, balance number format and so forth, without application and deeper layers having to know any of this. Similarly, we can refactor the database, or even switch from SQL to NoSQL, we can rename AccountService
to FetchAccountUseCase
and so forth, all without presentation layer having to know anything about it. All it will ever need to know is that it receives a DTO with account ID and account balance.
However, is this really what real development comprises - changing databases and text fonts? Real features, the ones giving real business value, involve changes that affect many (or all) technical layers. Consider this - presenting only account ID and balance will not suffice. Once the prototyping phase is over, we will want to present the actual account identification (IBAN or BBAN) rather than the surrogate ID it has in database. We might want to give customers option to name their accounts. To pass this data from UI all the way to the database and back again, we will need to add fields to our DTO “contract”. Any developer implementing a real feature will have to change presentation, application layers and the “contract”. Therefore they are not really decoupled. They are coupled externally. The “contract” does not help in this regard at all. It becomes just one more thing that needs to be changed with each additional feature.
Incomplete changes
One objection to this might be pointing out that as long as we only add new fields, the contract stays backwards compatible. We really can change only the layers from persistence to application, add a new field in the DTO, and presentation layer will still work. It will, but the feature will not be fully implemented. The feature will be fully implemented only when the new field is displayed in presentation. And to do that we have to change presentation. The point here is of course, that this objection entirely misses the point. The purpose of the DTO “contract” was supposed to be decoupling of presentation and application layers, which means organizing dependencies in such a way that a meaningful change could be fully implemented in one part, without affecting or having to know about the other part. Until the presentation is updated as well, the change is not complete and gives no value. The only situation in which this objection would make sense is when there are different people working on different layers. In that case the changes these different people (or even teams) are working on would really be contained within each technical layer and it is plausible that they would benefit from a DTO contract. However, structuring the work in such a way seems to be an extremely bad idea. Therefore, any change should be considered incomplete, unless it provides some real business value. And these changes tend to affect many layers most of the time.
Note that I am not advocating putting @Entity
, @Column
and @JsonProperty
annotations in one class, avoiding DTOs (although you should), or abandoning the Layered Architecture (although you should consider it). That is not the purpose of this post. The purpose of this post is to show that although some modules (classes, packages, microservices, etc.) might be predisposed to be easily split and and resulting parts naturally decoupled, not everything is. Some modules are already highly cohesive, and splitting them would only produce parts that are tightly coupled. Any attempts to introduce a “contract” between them would only change the type of coupling to external and make subsequent changes even more difficult.