When discussing service evolution, we have mentioned consumer contracts, and recommended building consumer side regression test cases to ensure that service updates won’t break any consumers. The consumer contract is not a new concept, and it was introduced in SOA architecture to address service evolution in XML schema. The same concept is still suitable for the microservices architecture, which is normally based on JSON and some RPC schemas.
A provider contract expresses a service provider’s business function capabilities in terms of the set of exportable elements necessary to support that functionality. From a service evolution point of view, a contract is a container for a set of exportable business function elements. A non-normative list of these elements includes:
Schemas are the parts of a provider contract most likely to change as the service evolves. In restful OpenAPI specification, they map to the definitions for the request and response.
In their simplest form, service provider interfaces comprise the set of exportable operation signatures a consumer can exploit to drive the behavior of a provider. Message-oriented systems typically export relatively simple operation signatures and push the business intelligence into the messages they exchange. In a message-oriented system, received messages drive endpoint behavior according to semantics encoded in the message header or payload. RPC-like services, on the other hand, encode more of their business semantics in their operation signatures. Either way, consumers depend on some portion of a provider’s interface to realize business value, and in consequence, we must account for interface consumption when evolving our service. In OpenAPI spec, interfaces map to operations/endpoints.
Service providers and consumers exchange messages in conversations with one or more message exchange patterns such as request-response and fire-and-forget. Over the course of a conversation, a consumer may expect the provider to externalize some state particular to the interaction in the messages that it sends and receives. Most restful APIs are stateless; however, there are relationships between different endpoints that form conversational conventions.
Besides exporting schemas, interfaces, and conversations, service providers may declare and enforce specific usage requirements that govern how the contract’s other elements can be realized. These requirements, most commonly, relate to the security in which a consumer can exploit a provider’s functionality. This maps to OpenAPI security definitions.
Quality of service
The business value potential that service providers and consumers exploit is often evaluated in the context of a specific quality of service characteristics such as availability, latency, and throughput. We should consider these characteristics as likely constituents of a provider contract and account for them in our service evolution strategies. There is no specific mapping in OpenAPI spec for QoS, but it can be defined in the same document as part of the extension.
The above list of elements defines a logical view of the service for its business function. Also, from a service evolution perspective, it defines a list of elements that we have to be very careful in changing as they are interrelated to each other, and one or more consumers might be impacted. Different types of services might have their provider contracts defined only as a subset of the elements. For example, an internal API contract might not have an explicit QoS definition.
If we decide to account for consumer expectations regarding the schemas we expose when evolving our service - and consider it worth our provider knowing about them - then we need to import those consumer expectations into the provider. The consumer contract can be reflected as a set of regression test cases that, if implemented by the provider, might help ensure the provider continues to meet its commitments to its clients. By implementing these tests, the provider gains a better understanding of how it can evolve the structure of the messages it produces without breaking existing functionality in the service community. And where a proposed change would break one or more consumers, the provider will have immediate insight into the issue and be better able to address it with the parties concerned, accommodating their requirements or providing incentives for them to change as business factors dictate.
In our example, we can say that the set of assertions generated by all consumers expresses the mandatory structure of the messages to be exchanged during the period in which the assertions remain valid for their parent applications. If the provider were possessed of this set of assertions, it would be able to ensure that every message it sends is valid for every consumer as the set of assertions is valid and complete.
By generalizing this structure, we can distinguish what we have already called the provider contract from the individual contractual obligations obtained in instances of provider-consumer relationships, which we will now call consumer contracts. When a provider accepts and adopts the reasonable expectations expressed by a consumer, it enters into a consumer contract.
Consumer contracts allow us to reflect on the business value being exploited at any point in a provider’s lifetime. By expressing and asserting expectations of a provider contract, consumer contracts effectively define which parts of that provider contract currently support the business value realized by the system, and which do not. This leads us to suggest that service communities might benefit from being specified in the first instance in terms of consumer contracts. In this view, provider contracts emerge to meet consumer expectations and demands. To reflect the derived nature of this new contractual arrangement, we call such provider contracts consumer-driven contracts or derived contracts.
Consumer-driven provider contracts’ derivative nature adds a heterogamous aspect to the relationship between the service provider and consumer. That is, providers are subject to an obligation that originates from outside their boundaries. This in no way impacts the fundamentally autonomous nature of their implementations; it simply makes explicit that services depend for success on their being consumed.
This is a paradigm shift in terms of service contract design, and the team must be careful not to drift to consume requests too much and miss the opportunity to explore the system capability from a service perspective. The designer needs to balance both client-side and service side requirements and restrictions in design.
Contracts may be expressed and structured in several ways. In their simplest form, consumer expectations can be captured in a spreadsheet or similar document and implemented during the design, development, and testing phases of a provider application. By going a little further and introducing unit tests that assert each expectation, we can ensure that contracts are described and enforced in a repeatable, automated fashion with each build.
As with the structure of contracts, we have several options when it comes to communicating expectations between providers and consumers. Since the Consumer-Driven Contract pattern is implementation agnostic, we could, given the appropriate organizational setup, transmit expectations simply by talking to other teams or using email. Where the number of expectations and/or consumers grows too large to manage in this manner, we may consider introducing a contract service interface and implementation into the connected systems’ infrastructure. Whatever the mechanism, it is likely communications will be conducted out-of-band and prior to any conversations that exercise the business functionality of the system.
Consumer-driven contracts offer two significant benefits when it comes to evolving services. First, they focus on the specification and delivery of service functionality around key business value drivers. Service is of value to the business only to the extent it is consumed. Consumer-driven contracts tie service evolution to business value by asserting the value of exportable service community elements - the things that consumers require providers to do their job. As a result, providers expose a lean contract that is clearly aligned with the business goals that underpin their consumers. Change - service evolution - only emerges where consumers express a clear need.
Of course, our ability to start with a minimal set of lean requirements and evolve our service changes when consumers demand that we are in a position to evolve, deploy and operate the service in a controlled and efficient manner. This is where the Consumer-Driven Contract pattern provides a second key benefit. Consumer-driven provider contracts give us the fine-grained insight and rapid feedback we require to plan changes and assess their impact on applications currently in production. In practice, this allows us to target individual consumers and provide incentives for them to relinquish an expectation that is stopping us from making a change that is not currently backward and/or forwards-compatible. By deriving our service providers from consumer contracts, we can have a repository of knowledge and a feedback mechanism that we can draw on during the operations part of the system life cycle.
In this article, we’ve identified the motivation for introducing consumer-driven contracts into the service landscape. Thereafter, it described how the Consumer-Driven Contract pattern addresses the forces that determine service evolution.
The Consumer-Driven Contract pattern is applicable in the context of either a single enterprise or a closed community of well-known services. More specifically, it is an environment in which providers can exert some influence over how consumers establish contracts with them. No matter how lightweight the mechanisms for communicating and representing expectations and obligations, providers and consumers must know about, accept, and adopt an agreed-upon set of channels and conventions. This inevitably adds a layer of complexity and protocol dependence to an already complex service infrastructure.
We’ve suggested that systems built around consumer-driven contracts are better able to manage to break changes to contracts. But we don’t mean to suggest that the pattern is a cure-all for breaking change: when all is said and done, a breaking change is still a breaking change. We do believe, however, that the pattern provides many insights into what actually constitutes a breaking change, and as such, may serve as the foundation for a service versioning strategy.
Consumer-driven contracts do not necessarily reduce the coupling between services. Loosely-coupled services are relatively independent of one another but remain coupled nonetheless. What the pattern does do, however, is excavate and put on display some of those residual, “hidden” couplings, so that providers and consumers can better negotiate and manage them.
The underlying assumption here is that services, by themselves, are of no value to the business; their value is in being consumed. By specifying services closer to where they are being used - by consumers - we aim to exploit business value in a lean, just-in-time fashion.
Finally, we should point out that there is a risk that allowing consumer contracts to drive the specification of a service provider may undermine the conceptual integrity of that provider. Services encapsulate discrete, identifiable, reusable business functions whose integrity should not be compromised by unreasonable demands falling outside their mandate.