Skip to content

Instantly share code, notes, and snippets.

@benjchristensen
Last active December 20, 2015 02:29
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save benjchristensen/6057002 to your computer and use it in GitHub Desktop.
Save benjchristensen/6057002 to your computer and use it in GitHub Desktop.
Consumer Driven Contracts: notes, links, quotes and thoughts

Problem Statement

A service layer (Java API in this case) is backed by dozens of services (over the network in an SOA) and used by dozens of clients, each using different subsets of the overall API. In our case all client interaction happens within a single JVM so we can know which clients are accessing what services, fields and keys (versus a RESTful API over HTTP where this would be unknown).

Principally the API is functionality focused rather than system focused. A client should request data to serve a given use case and not need to concern themselves with the underlying implementation details of what backend system the data is coming from.

Today this is achieved through static typing and Java object modeling. Types from underlying services are decoupled from clients so that backend systems can change their implementations without impacting clients.

This means (assuming Java language environment):

  • All backend services are wrapped by objects that originate in the API Service Layer so that underlying dependencies are not leaked. This is done so that backend type systems are decoupled from frontend clients.
  • Types end up being validated at deserialization time in the network client.
  • Documentation is generated from the static object model as Javadocs.
  • Code completion is enabled by static typing in IDEs to help with discovery.

Problems are:

  • Static typing does not communicate or enforce entire contract
  • Static types tell us the 'shape' of the data but not the possible values, lengths, ranges, existence etc
  • All backend services must be wrapped by the API Service Layer which means any type change (including field addition) involves the API Service Layer team being involved.
  • Production breakage can still occur when backend systems change behavior and values that the static typing does not catch, or that happens at the remote server and breaks at deserialization time.
  • Static types couple modules together as they leak across boundaries. This is why new types must be created and wrap around underlying services to decouple the layers.

System Requirements

We want to enable rapid iteration while balancing safety concerns.

This means:

  • We want backend services to be as loosely coupled as possible and be able to change as much as they can without the API changing.
  • We want services to know if they have changed something that breaks a client before it goes out to full production traffic.
  • We need services to have a deprecation lifecycle to deprecate something, communicate to users to migrate off and know when it's no longer being used so they can remove it.
  • We would like the API Service Layer to:
    • expose functionality instead of infrastructure to clients
    • enforce common abstractions such as rx.Observable for asynchronous execution and composition
    • get out of the way of services adding/removing/modifying data as long as client expectations and requirements are met
    • provide clear documentation of all available functionality include what to expect from the data and how to code against it
    • protect against underlying services breaking the contracts

Possible Solution

A possible alternative solution is making static typing optional or non-existent and using "consumer driven contracts" or specifications of some kind to achieve safety and discovery requirements while enabling rapid iteration

The clients and services would achieve a higher decoupling and the API Service Layer would stop defining the types and shape of data, thus decoupling it from the everchanging shape of data flowing through it. The API Service Layer would still remain a broker of connectivity and functionality and a tool for asserting correctness and safety but it would enable this without injecting artificial layers of types between clients and services.

The contracts need to address the following aspects of data:

  • possible values
  • what elements are being used
  • example usage
  • assertions on data values
  • versioning and deprecation (end-of-life + migration)

We should be able to use them to:

  • generate documentation
  • generate examples
  • execution functional tests

We should use metrics to track exactly what keys are being used by what endpoints and derive "implicit contracts" for clients that tell the system what each client is consuming.

Clients will get implicit contracts automatically by using the API Service Layer which can be used to communicate to services what keys/fields are being used and thus can't be removed or changed. Clients should also be able to provide explicit contracts with assertions of the data they expect and need for their client to work. It should be possible for all assertions by clients for a given API function/method to be aggregated into a view of what that function is required to do and perform functional testing in continous build and deployment processes as well as autogenerate documentation for clients and reveal to backend service owners how their data is being used. These same assertions could become unit tests for services.

In summary, the API Service Layer would "connect the pipes" between services and clients with common abstractions (behavior and functionality instead of infrastructure, Observable, etc) but ignore the "shape" of the data passing between them and allow the clients to define what is expected implicitly and explicitly.

Instead of compile time checks (which only account for a portion of failure states anyways) the API Service Layer system would provide tools that:

  • assert correctness in dev/test/prod environments by executing functional tests derived from explicit and implicit contracts
  • expose usage information to services for use in unit and functional tests
  • enable lifecycle management for deprecating keys/fields/functions, communicating to clients using them and reporting to services when all use has stopped
  • automate documentation generation including not just available keys/fields but assertions about the expected data for each key/field and possibly tests showing sample usage

Lifecycle:

  1. New Functionality/Service

If a new service or functionailty needs to be added to the API Service layer, the API team would be involved in exposing this as a new method. For example:

Observable<Map<String, String>> getSocialFriends(User user)

It would not however create an artificial type hierarchy to represent the return type, it would allows dictionaries of data to be returned.

The API team could include default assertions and contracts that are known to be global and then client teams could add more as they see fit.

  1. New Key/Field

The team controlling the backend system for getSocialFriends functionality could add new key/value pairs whenever they want and it would just start flowing through the system without any changes.

  1. Remove Key/Field

The backend team could stop sending a key and that could break clients if it was depended on.

Safety against this (today only done at compile time, but not if the backend service deploys and changes the network response) would exist as runtime functional tests. The service team could apply these assertions to catch the mistake of deleting a key that is in use. If they do not apply these assertions then runtime assertions in the API system would catch them and alert.

The API system would provide tools so the backend team knows what keys are used.

  1. Change Key/Field Behavior

Similar to removing, using the API tools they can see who is using the keys and determine what kind of change can be performed if any.

Literature

Presentation on core.specs (a Clojure library) and how a specification can enable things such as:

  • contracts
  • generative tests
  • typing
  • model-checking
  • example usage
  • enhanced documentation

“Types are inherently non-local - they describe data that flows across module boundaries.”

A business can only fully realise these benefits, however, if its SOA enables services to evolve independently of one another. To increase service independence, we build services that share contracts, not types.

...

Our service community is frustrated in its evolution because each consumer implements a form of "hidden" coupling that naively reflects the entirety of the provider contract in the consumer's internal logic. The consumers, through their use of XSD validation, and to a lesser extent, static language bindings derived from a document schema, implicitly accept the whole of the provider contract, irrespective of their appetite for processing the component parts.

...

David Orchard provides some clues as to how we might have avoided this issue when he alludes to the Internet Protocol's Robustness Principle: "In general, an implementation must be conservative in its sending behaviour and liberal in its receiving behaviour". We can augment this principle in the context of service evolution by saying that message receivers should implement "just enough" validation: that is, they should only process data that contributes to the business functions they implement, and should only perform explicitly bounded or targeted validation of the data they receive - as opposed to the implicitly unbounded, "all-or-nothing" validation inherent in XSD processing.

...

Here then is a relatively lightweight solution to our contract and coupling problems, and one that doesn't require us to add obscure meta-informational elements to a document. So let's roll back time once again, and reinstate the simple schema described at the outset of the article. But this time round, we'll also insist that consumers are liberal in their receiving behaviour, and only validate and process information that supports the business functions they implement (using Schematron schemas rather than XSD to validate received messages). Now when the provider is asked to add a description to each product, the service can publish a revised schema without disturbing existing consumers. Similarly, on discovering that the InStock field is not validated or processed by any of the consumers, the service can revise the search results schema - again without disturbing the rate of evolution of each of the consumers.

...

[On consumer contracts] … 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 in fact 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.

...

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. In more sophisticated implementations, expectations can be expressed as Schematron- or WS-Policy-like assertions that are evaluated at runtime in the input and output pipelines of a service endpoint.

...

We've suggested that systems built around consumer-driven contracts are better able to manage breaking changes to contracts. But we don't mean to suggest that the pattern is a cure-all for the problem of breaking changes: when all's 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. Moreover, as we've already discussed, service communities that implement the pattern are better placed to anticipate the effects of service evolution. Provider development and operations teams in particular can more effectively plan their evolutionary strategies - perhaps by deprecating contractual elements for a specific period and simultaneously targeting recalcitrant consumers with incentives to move up to new versions of a contract.

...

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 idea [of Janus] was to write the contracts in such a way that as well as using the contract to verify the behaviour of the service, use that very same contract to provide mocks to the consuming applications. Executing the contract in one mode would test the service; executing it in another mode would create an out-of-process mock server. As soon as the client team writes the contracts, they would get immediate value out of it.

(service
  "Service name"

  (contract "contract name"
    (method <one of :get, :post, :put or :delete>)
    (url "full, absolute URL to the service")
    (header "header name" "header value")
    (body <:xml, :json or :string>
          <Clojure data structure that will be serialized as above>)

    (should-have :path "json path" :matching <regex>)
    (should-have :path "json path" :of-type <:string, :array, :object or :number>)
    (should-have :path "json path" :equal-to <value>)
  )
)
@jcacciatore
Copy link

The current model is more suited to mature clients that have established domain models. For new services it is too slow and cumbersome. You may want to add a discussion point about the philosophical difference of the service layer providing the 'business view' for UI clients into the domain model as opposed to mid-tier services providing that view which may be an additional use-case for them.

Problems are:

  • Static types tell us the 'shape' of the data but not the possible values, lengths, ranges, existence etc

This is not strictly true - you can put custom validation in your serialization. The bigger issue with this in my mind is that when this occurs this contract is hidden and can surface unexpectedly at runtime if clients aren't aware that it has been put in place.

Another problem that you may want to consider adding to the 'Problems are' section is that static typing involves process overhead that we no longer want to incur. Each time a new client releases a revision we must 'promote our dependencies' to pick up that revision, make API Service Layer changes accordingly which itself may may be significant to understand the change, and from there go through our build and deployment processes.

One of the current value propositions of the API Service Layer is the intention to provide a cohesive api. I allude this above in my first comment about business views. An example of this is that some downstream systems return raw data to us (e.g. id's) that are then transformed by the service layer into an APIVideo or some other entity. Exposing this to script writers would have both pros and cons that I haven't fully thought through... More than likely we would need to work with those systems to provide dictionaries of useful metadata rather than a simple id. You may want to address where this fits into the solution with Data Contracts.

@benjchristensen
Copy link
Author

you can put custom validation in your serialization

That is not the static typing or compiler doing the work, the fact that code is being written to validate the data at deserialization time is exactly the point of data contracts and they are a runtime thing not offered by static compilers.

Each time a new client releases a revision we must 'promote our dependencies' to pick up that revision

Good point to document.

You may want to add a discussion point about the philosophical difference of the service layer providing the 'business view' for UI clients into the domain model as opposed to mid-tier services providing that view

Interesting perspective. I imagine we can have a mixture of the two, or even three.

  • service provider delivers assertions for the base contract of things they expect everyone to use
  • api team can add further assertions
  • client teams adds assertions that fit their exact needs

One of the current value propositions of the API Service Layer is the intention to provide a cohesive api.

How often does data from different services actually overlap in such a way that they can or should have the same keys/fields?

I understand that if we have compound objects that embed something like video metadata then it should always be the same, but it will always be the same regardless of typing since it comes from the same service.

What is a use case where a common dictionary/object/type is used to represent multiple services? If they are different services it seems they are for a reason and would have different data being returned.

@benjchristensen
Copy link
Author

Some feedback I received was to add more concrete examples of the following:

  • what would the contracts and assertions looks like?
    • the Beyond Contracts presentation and Janus project give good examples but at some point I'll try and add some here applicable to our use cases
  • what would auto-generated documentation look like and what could be derived?
  • what information could implicit contract actually derive from runtime behavior?
  • what would the architecture look like that would allow capturing runtime information for implicit contracts?

All of these are valid and I'll pursue them as I dive deeper into implementation from the theory I'm discussing above.

@daveray
Copy link

daveray commented Jul 25, 2013

Another "problems are" item:

  • To do their current job, the API Service Layer team have to effectively become domain experts of the backend service their wrapping.

Adapting an API created by an arbitrary "loosely coupled, (hopefully) highly aligned" team to fit cohesively within the service layer will require a pretty good understanding of not only what the backend is exposing, but also the use cases of clients. That's a tough position to be in, especially under deadline.

@daveray
Copy link

daveray commented Jul 25, 2013

I've read this three times now and, since I'm kinda dumb, I'm still digesting. One thing about Ben's comment:

it will always be the same regardless of typing since it comes from the same service

I think this is raises an interesting question about the service layer: Is it an interface, or an abstraction? Ben's comment seems to imply it's an interface, i.e. each part of the SL is basically a wrapper around a particular service and types are associated with that service. The SL provides a consistent, cohesive way of coding against a bunch of services, but it doesn't do much to abstract or "take away" from the functionality those services provide. Turning int into APIVideo is basically just sugar.

On the other hand, as Jason notes, if the SL is a "business view" aggregating multiple services into a use-case-driven facade for clients, then it's an abstraction. It exposes enough for the clients to do what they need to do.

In reality, the SL is probably a bit of both. For well-established use cases, it's more of an abstraction, reducing the number of fiddly details a client developer has to keep track of to do the same thing a dozen teams have done before. If they need to do something more, they're stuck.

For newer services, it's just an interface. Everything's in flux so the best the SL can do is act as a pass-through and try to stay out of the way. Here clients probably have to do more fiddly coding since the API is more raw. But, both ends can evolve rapidly.

What's the point? Maybe thinking of where API lies on this continuum (is it a continuum? can both extremes be achieved?) and where we want it to be could guide the discussion/design/etc. I don't know, maybe I just said what Jason and Ben said in a different way :)

@benjchristensen
Copy link
Author

I agree that it's both and not valid to attempt to comply with only one or the other.

I feel similarly about static versus dynamic typing at this layer, I think we should allow mixed typing.

For some pieces of functionality it may make sense to retain statically typed abstractions whereas others may want dynamically typed interfaces (more or less pass-thru) and sometimes a mixture (typed objects with fields and then key/values for extension).

I think the important part is to stop using the single tool of static typing to try and solve the safety and discoverability use cases, particularly since it doesn't solve the full problem anyways. Static typing can still be a tool and used where it makes sense, but this discussion is about exploring a solution where that's all it is, an optional tool rather than the rule.

I'll try and put some more concrete examples and diagrams in...

@jcacciatore
Copy link

@daveray - thanks - you nailed it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment