Some time ago, we had a discussion in one of our projects whether it was the right time to introduce the Consumer-Driven Contract (CDC) approach. After a short debate, we came to the conclusion that while CDC may be a very useful tool, it’s not a silver bullet. It’s extra to what we already have and makes some processes much faster and less error-prone.
Normally, as a part of the API developers prepare some documentation like a swagger or wiki page. They specify all the possible input values with information on whether a particular parameter is mandatory or not. They do the same with the returned value. All values returned by the API are described in this document. We share documentation with third parties (or with other teams inside a company), and that’s all.
There is no space for feedback from the API’s user. We don’t know how our API is used on the data level. We probably have some monitoring in place, and we know how often users are calling our service. But do they use the entire response? Or maybe just one field? As in many other places, a short feedback loop may bring value here.
CDC is nothing else than the feedback from users about how they’re going to use our API. CDC is all about reversing the most common approach I described above. With CDC, the contract is not documentation written by the provider, but information from the consumer describing how they use the API.
Contracts over communication and documentation?
Does it mean that someday instead of receiving an email from someone who is going to use our service or ask us for something custom we’ll receive just a formal contract? Or does it mean that the consumer will define the API and we will just have to implement it? Of course, in most cases, that’s not going to happen.
The CDC doesn’t remove the need for good documentation. And for sure, it doesn’t replace face-to-face communication with a formal contract. Especially the second element - communication - is crucial for implementing the CDC successfully. In most cases, the CDC is only about feedback on how people use our API. We’re still responsible for providing documentation and discussing the details if the API we’re providing is something new. Sometimes not only the contract but the entire API may be driven by the consumer, using the same tools (some kind of formal contract). It all depends on our process and relations between the consumer team and the producer team.
Why is it so important?
How risky is refactoring code without test coverage? We would have to go to each place where the refactored function is used and check whether everyone is working in some way. Sometimes, it’s too expensive and risky so developers might give up.
Here we have a similar situation but in the HTTP world. Since we may not work with all the API consumers in the same office, such verification is usually even more expensive. Even in the same location, asking all developers to check the code because we are going to fix a typo in one of many fields returned by our API (which may be never used by someone) doesn’t sound good. By keeping contracts from all consumers in one place, we can quickly check whether anyone uses a specific parameter. We can safely modify our code without thinking about backward compatibility. The CI won’t pass any element that breaks any of the existing contracts.
It may sound as if we’re looking for a quick fix instead of proper API versioning. But API versioning is very expensive and complicated. We should avoid it for as long as possible (Zalando https://martinfowler.com
). The CDC may allow us postpone that moment when versioning is the only solution.
Another aspect, in some cases an even more important one, is testing distributed systems. When we’re writing integration tests for a single service, we have two options - set up all dependencies or mock them. Since a contract consists of input and expected output, it can be used for automatic stubs generation. Having stubs for all the dependent services, we can easily prepare an isolated test that doesn’t take hours to start or need as many resources as integration test setting up many services.
What if we don’t know our consumers? Of course, we can’t ask them to prepare contracts. It means the CDC isn’t going to work in our case. In such a situation, we have to treat our documentation as a contract and be very careful when modifying anything resulting from changes to API. We have to assume that all the data we return are consumed by someone. We need to keep backward compatibility on the basis of our documentation, not specific use cases.
The second case when the CDC may not be a good fit is when we’re working with a public API. Even if we know all of our consumers, it may be really difficult, or in some cases impossible, to force them to spend time on preparing a contract and keeping it updated. It’s sometimes the matter of a paper-based contract signed by a business that states the expectations of both parties. In such cases, the CDC may not be the best solution. The biggest benefit of using the CDC will be visible if both sides of the contract work at the same company and share the same development practices.
One of the most important steps in CDC is sharing the contract written by the consumer with the producer. It can be done, for instance, through a merge request containing a contract in a format supported by the producer. Contracts may be also shared using a special tool, for instance, Pact (Pact Broker). Pack is a great tool that offers much more than just sharing contracts. It supports the entire CDC flow, including testing consumers using stubs created based on contract, sharing the contract, and verifying the contract on the producer side. Another tool that may be helpful here is the Spring Cloud Contract. You may also use Pact Broker as a contract repository.
Contract-Driven Design and contract testing are two useful tools that bring stability to distributed systems. They allow getting rid of late validation of problems regarding the integration between services. It doesn’t require deploying a service and checking integration manually. The feedback loop is quite short and can stop the building process before an artifact is created. It’s also good documentation showing who uses the API we provide and how they do it.