Principles of Service Design: Program to an Interface

December 05, 2011 · 4 min read

I’ve been thinking a lot about service design recently, and one of the trickier problems is deciding how to implement and version a service so that supporting (or dropping) older versions is straightforward. It turns out that advice originally meant for writing code works brilliantly for writing services too: program to an interface.

Program to an Interface

Borrowing from many blogs and books, I’ve come to believe that viewing a service interface the same way you’d view a programming interface is the right move. Interfaces can be versioned. They isolate client code from the implementation behind them. And once published, a given version should be immutable.

A service should hide its implementation details. If a database table changes inside the application providing the service, the clients of that service shouldn’t have to care.

Just like interfaces in a programming language, by specifying a well-known interface to a service we free ourselves from worrying about how clients interact with it. When we need to change the implementation, we can do so without breaking anyone. And the reverse is also true: clients don’t need to worry about implementation changes as long as the interface stays consistent.

Of course, interfaces sometimes have to change to support new functionality. When they do, we want to be confident we’re using the right version. Just because v2 has been released doesn’t mean our clients automatically support it. We want to keep using v1 until we’re ready to update. Versioning gives us that choice.

Beyond URIs

When we think of a service, we usually think of a web service. In these RESTful days the interface is generally thought to be the combination of URIs we interact with. But that’s not the full picture. Supporting an interface doesn’t just mean your URIs are stable between versions — it also means the content returned from service calls (i.e. HTTP responses) conforms to a defined structure.

And of course, a web service is only one kind of service. Plenty of services don’t have a web interface at all, usually in situations where synchronous request-response messaging isn’t appropriate. Order processing, inventory management, fraud checks — these might take several seconds and are better handled asynchronously. The interfaces to these services should be versioned for the same reasons a web service’s should.

The version of the interface should be detectable with each message passed or received, no matter the transport. We should know that we’re dealing with version 3 of an API, not guess.

Mime types to the rescue

Handily, versioning interfaces for these types of services is pretty much the ideal use case for a MIME type, and most message transports support custom MIME types:

MIME types have a space reserved for vendor-specific types, application/vnd, inside which we’re free to define our own. There are a few conventions to follow to avoid name collisions: include your organisation name, a very short description of what you’re representing, a version number, and a base format.

A worked example

Say you work at the Acme Toy Company. When your web service accepts an order via its RESTful interface, it puts a message on a queue with four fields — customer_id, purchase_order_id, amount, and description — in JSON:

{
  "customer_id": 123,
  "purchase_order_id": "ASLA-001-2031",
  "amount": 1000,
  "description": "100 x Acme Toy Dynamite"
}

We coin a MIME type, application/vnd.acme.order-v1+json, and publish an interface specification saying any message claiming to be this type will have these four fields. Then in a consumer of the orders queue we use the Selective Consumer pattern to subscribe only to messages of this MIME type. Inside the consumer we can be confident that we’ll only receive orders in a format we understand and can process. Partners can POST with this MIME type in the Content-Type header so everyone knows what they’re talking about all the way through the system.

A few months pass. Several partners are using the order API, but we want to automate our stock inventory, so instead of a plain text description we want item IDs. We don’t want to force this change on our partners, though — their development cycle is slow and they’re sending us plenty of orders. We like their cash.

So we publish a second version, application/vnd.acme.order-v2+json, defining messages like this:

{
  "customer_id": 123,
  "purchase_order_id": "ASLA-001-2031",
  "amount": 1000,
  "items": [
    { "item_id": 1032, "quantity": 100 }
  ]
}

It’s now trivial to add a second Selective Consumer that handles only v2 messages and updates inventory accordingly. The v1 consumer keeps running happily with v1 orders. There’s a smooth, unhurried migration path for clients from v1 to v2. We can support both versions or drop older ones as we choose. We could even use a combination of Splitter, Translator, and Enricher to route v2 messages into the v1 consumer while splitting off inventory management messages to a separate, lightweight consumer. None of this matters to our partners, because they know they’re working to the interface we’ve defined.

When the transport changes

We might eventually decide that the RESTful order service isn’t appropriate for v3 — perhaps we’ve been won over by WebSockets. When we receive an order claiming to be v1 or v2 on the RESTful service, we can still happily accept it. If we receive anything else, we return an HTTP 406 to tell the client we can’t accept orders that way for v3.

In contrast, if we’d used plain application/json we’d have to guess based on message fields which version the client intended. That’s barely practical with the trivial example above, and once there are several versions of the interface it becomes a nightmare.