Contract-Driven Development (CDD) in Microservice Architecture: An In-Depth Technical Guide
David Shergilashvili
Head of Software Development Unit at TeraBank | ?? T-Shaped .NET Solution Architecture
Introduction
Microservice architecture has become a de-facto standard for modern enterprise applications, especially in the financial and banking sectors. It allows us to break down monolithic applications into small, well-defined services that can be independently built, deployed, and scaled. However, this architecture comes with its challenges, particularly in terms of potential integration issues between different services. The Contract-Driven Development (CDD) approach offers a way to prevent and detect these problems early.
In this article, we will step-by-step examine how CDD can be implemented using a real-life example from the banking domain. Our goal is to explain all the key concepts and aspects of CDD, including creating API contracts, using mock services, and writing contract tests.
Step 1 - Understanding the Business Requirement
Our task is to create a part of an online banking system responsible for ordering new credit cards. This process involves several sub-services:
Each of these services may have its own team and tech stack. Our goal will be to integrate them in a way that adheres to the "loose coupling" principle and reduces the propagation effect of changes. This will allow each team to work as independently as possible.
Step 2 - Creating API Contracts
The first step in the CDD process is to develop API contracts or specifications in coordination with all involved parties. This contract represents an SLA (Service Level Agreement) between the provider and consumer services - it describes how the consumer should send requests to the provider and what response it should expect from the provider. This description includes Endpoints, HTTP methods, request/response schemas, HTTP status codes, etc.
The most popular formats for describing such contracts are OpenAPI (formerly Swagger) for synchronous REST APIs and AsyncAPI for asynchronous messaging systems.
Example of a contract in OpenAPI format:
According to this contract, the Orders Service has a POST method on the /orders endpoint, which accepts a CardOrder object in the request body and returns an OrderCreated object with a 201 status code. If the request is invalid, it returns a BadRequest object with a 400 status.
The contract should be agreed upon by all parties (in our case, the teams of Orders, KYC, Scoring, Provisioning, and Notification services) and stored in a central Git repository.
Step 3 - Creating Mock Servers Based on Contracts
Once we have the agreed-upon contracts, the next step is to use them for Mock implementations. Mocks will allow consumers to start building their applications without waiting for the real services to be ready. The mock server exposes the same API as the real service will do in the future according to the OpenAPI specification, it returns artificially created (dummy) data.
Popular libraries and tools that automatically generate mock server code from OpenAPI specifications are Prism, WireMock, Mountebank, and others. It's also possible to create custom mocks, for example using the Moq library for . NET.
Example of setting up a mock server with Prism:
npm install -g @stoplight/prism-cli
2. Run the mock server on our OpenAPI specification
prism mock credit-cards-api.yaml
Now we will have a mock server at https://127.0.0.1:4010, which we can address with HTTP calls from Postman or code and always receive a valid response (though with dummy data).
Example of creating your mock with Moq in C#:
领英推荐
Here we create a mock for the ICardProvisioner interface and write the logic of what it will return when the ProvisionCard the method is invoked. This mock object is then passed to the constructor of the real OrderService, which will be used instead of the CardProvisioner.
Step 4 - Writing Contract Tests for Provider Services
In parallel to the consumer service application being developed with mock servers, the provider service teams should write integration tests that will verify that their service adheres to the OpenAPI contract.
For example, the code for contract tests for the KYC service might look like this (C#, NUnit):
Here we write two test methods for the KYC controller's ValidateClient endpoint - one for the success case (when the client exists in the database), and the other for the case of the client's absence. In the tests, we check that when the required parameter is provided, we receive a 200 OK response with the correct structure of the body, and when the client does not exist, 404 Not Found is returned. That is, the real implementation fully covers the cases provided for by the OpenAPI contract.
Step 5 - Integration
When all teams finish writing contract tests and they pass successfully, it means that the real implementation of all services complies with the agreed contract. After that, we can start integrating them.
We replace mock services with real ones by changing their base URLs in the configuration. For example, if before the Orders Service connected to the mock version like this:
PROVISIONING_API_URL=https://mock-provisioning-service.mycompany.com/api/v1
Now we will change it to the URL of the real one:
PROVISIONING_API_URL=https://provisioning-service.mycompany.com/api/v1
Similarly, we gradually replace the addressing of mock servers in the code.
At this stage, we are almost guaranteed that the services will work together properly because everyone has agreed to the contract. We can run minimal end-to-end or smoke tests to make sure, but any large-scale integration testing will no longer be necessary.
With this approach, the entire development process is greatly simplified and accelerated, because:
Of course, in practice, not everything is always so ideal. API contracts may not be comprehensive, change over time, or despite the existence of a contract, some service may not "comply" with it. Also, the CDD approach does not cover all types of integration. But despite this, it is still very useful to avoid problems discovered at a late stage and to facilitate collaboration between teams.
Conclusion
Contract-driven development is a powerful tool for addressing the challenges of transitioning to microservices. In this article, we step-by-step examined aspects of its practical implementation, such as:
Of course, the effectiveness of CDD largely depends on how well and in detail the API contracts are developed, how much all teams adhere to them, and how they respond to changes. That is why the implementation of CDD requires not only technical changes but also organizational ones - APIs should be perceived as a first-class citizen and the whole team should feel responsible for their quality.
With the right approach, CDD will contribute to the fast and high-quality implementation of projects, where all teams effectively collaborate through APIs. This is especially beneficial for the financial and banking sectors, where systems are becoming more complex and multi-component every day, while quality and on-time delivery are crucial.
Enterprise/Solution Architect: Integration, Governance, Digital Transformation, Services, Security
1 个月Having years in services, API design and banking, I'd like to comment on this article step-by-step. Step 1. - it is not clear what is "KYC regulations" and what is included in there, i.e. whther user's status (prospect or existing) and several residency and living addresses are included - the Financial Anti-Fraud control is absent in the list and, frequently, in the real world (including Sanction Checking and AML regulations). Step 2. - the illitracy of modern developers is amaising - it seems that they are the first guys in the town of IT and continuously "discover a bicycle or America" - depending on who likes what. In the case of this article, a service (even a leave service such as Microservice, has an intrface that should be used to communicate/involve integrate with it. The classic term API defines the interface as well. Thus, specifying the API is equal to specifying interface, as the first. Thus, CDD is nothing than IDD -Interface driven design.