Consumer-driven contracts (CDC) is a pattern for specifying and verifying interactions between different modules of an application. Consumer-driven means that it is the responsibility of the Consumer to specify what interactions it is relying on, as well as their format. Other services must then agree to these contracts and ensure that they are not breaking them.
Interfacing between services
The monolithic way
In a monolithic application (one very big project), different modules live side by side. What differentiates them is likely the simple fact that they are in different packages, or different jars.
In this type of scenario, Integration Testing can be done almost like standard Unit Tests. Modules can be instantiated and wired up at runtime in the same Test case. While these tests might verify some functional requirements, they are also verifying that the modules are interacting with each other through a verified API. This is pretty invisible because the compiler itself would reject the tests if, for example, a method signature had changed but wasn’t updated everywhere.
The microservices way
In the case of microservices, different services are no longer wired together under the same runtime. Changes in the exposed interfaces for these services can no longer be caught by the compiler.
Example: Catalog Service checking the availability of items with Stock Service.
Without the necessary precautions, there are a lot of ways these interactions can be broken by changes made in the different services. The most common one would be that the Provider would change its interface in such a way that the Consumer can no longer interact with it.
- Change of the endpoint URL (e.g. GET /stockLevels renamed to GET /stockLevel)
- Change in the expected parameters (e.g. GET /stockLevels expecting a new mandatory “category” field)
- Change in the response payload (returns an array, instead of having an array wrapped in an object)
While the changes above clearly look like API-breaking changes, it might be a lot less obvious that some minor code changes could affect the structure of the payload. This can be as implicit as changing some field name in a class that will later be serialized into JSON to construct the response.
Where previously the breaking change would have been caught at compilation time, it is no longer the case. It won’t be caught at runtime either for the provider service : as far as it is concerned, it is behaving without errors, responding to defined end-points with defined data.
The problem will only start arising on consumer services, once the provider service is being updated to a new version. This is where things get nasty to debug in a production environment: a service gets updated and is behaving perfectly, but suddenly a number of other services start behaving incorrectly. It highlights a critical breaking point of such architecture: consumer services would live in constant fear of interface changes in some service they rely on for their activity.
Obviously this situation should never happen in production as we should ensure that we test all one-to-one interactions between services. This is where CDC helps.
Integration testing between microservices
Integrating microservices the traditional way
The most obvious way to verify if 2 services work together is to start both services and verify that the communication between them is working as expected. However, there are some issues with that approach.
To begin with, the consumer service must know how to start the provider service. This sounds like unnecessary information, likely difficult to maintain when the number of services start ramping up.
Then, starting up a service can be slow. Even if we’re only talking a few seconds, this is adding overhead to build times. If a consumer depends on multiple services, this all starts adding up.
Another problem is that the provider service might depend on a data store or other services to work as expected. It means that now not only the Provider needs to be started but also a few other services, maybe a database… before we know it, we’re starting an end-to-end test environment just to test the interaction between two single services!
Using contracts to verify service interactions
If only we didn’t need to start the provider service and still have integration tests… this is where CDCs come in!
The concept behind CDCs is to split the work of the integration tests right in the middle, i.e. at the communication stage between the 2 services.
I like the description of CDCs as “asynchronous integration tests”:
- The consumer defines what it expects from a specific request to a service
- The provider and the consumer agree on this contract
- The provider continuously verifies that the contract is fulfilled
This implies a few things:
- Consumers need a way to define and publish contracts
- Providers need to know about these contracts and validate them
- Consumers and provider might have to agree on some form of common state (if the provider is not stateless or depends on other services)
Using Pact for CDC testing
Pact is a library that enables CDC testing.
Pact lives in both Consumer services and Provider services and is implemented in a number of different languages, which makes the integration between services using different technologies possible. It relies on Pact files, published in JSON format, making it easy to implement custom Pact generation or verification.
Step 1: The Consumer generates the Pact
The consumer declares that it has a contract with a given provider. This contract specifies a given interaction with that provider, using the Pact domain-specific language.
It states what request will be sent, what response is expected and what the body of the response is. This is the definition of a Pact Fragment, which will be used by Pact to mock up the Provider Service. In a unit test, the consumer then triggers the interaction using its usual code base and verifies that the result is correct.
If the unit test passes, a Pact file is generated. This Pact file should then be handed over to the Provider service (it might be a manual copy, a reference by URL or an third-party broker such as Pact Broker).
Step 2: The Provider verifies the Pact
Once the Consumer has generated contracts and advertised them to the Provider it is using (or the Providers if there are multiple ones), it becomes the responsibility of the Provider to ensure that these contracts are fulfilled and never broken.
There are different ways to validate the Pact on the Producer side.
One approach is to start the Provider service and replay the request specified in the Pact file and ensure that the response is matching. This is not very practical if the Provider service is not stateless or depends on other services.
The other approach is to include the Pact testing as part of the Producer unit tests. As part of their contract agreement, the Consumer and Producer can agree on some common state that will be used to generate the Pact files. In our Java projects, we are using the pact-jvm-provider to read Pact files, in order to replay the requests and verify that:
- The API endpoint is valid
- The response format is accepted and contains the same data (from the commonly agreed state)
Why is it better?
No need for multiple services to be started up and wired together
Managing dependencies and service discovery is a non-trivial overhead. Starting multiple services takes time, making builds longer and therefore making deployment turnaround longer. If we can verify the exact same thing faster, it’s a win!
Consumers are responsible for the creation and maintenance of their contracts
If some interactions are absolutely crucial to some consumers, creating and publishing such contracts is a great way to make this public knowledge to providers. Consumers might not want to publish contracts for every single interaction, but they definitely do for the important ones… and who better than the consumer to know what matters most?
Providers have better peace-of-mind when making changes
While microservices is a great way to deploy changes fast, the implications of small changes can become scary when a Provider doesn’t know for sure how other services might be relying on it.
The fact that the Provider can have a list of contracts coming directly from Consumers will bring peace-of-mind that these important interactions will continue working with the proposed changes.
CDC testing is a big deal for microservices. Being able to verify contracts at build time, without having to start up services and their dependencies, saves a large amount of time.
In my opinion, giving the responsibility to the consumer to define contracts is one of the great things about CDCs. Consumers will (and should) be paranoid about the services they interact with. Publishing contracts for them is a way to publicly say “don’t you dare break this in your next version”... which is a lot better than for consumers to silently hope that the Providers will meticulously verify the non-regression of their API.