Service Versioning Illustrated

Hogarth painting the muse

My last post, “No Structure Services”, generated some discussion on LinkedIn regarding service versioning and how an application’s architecture can enable exposing services that are well-defined, stable, internally simple and DRY. I’ve discussed these topics in the past: “Strict Versioning for Services – Applying the Open/Closed Principle” detailed the versioning process I use to design and maintain services that can evolve while maintaining backwards compatibility and “On the plane or in the plane?” covered how I decouple the service from the underlying implementation. Based on the discussion, I decided that some visuals would probably provide some additional clarity to the subject.

Note: The diagrams below are meant to simplify understanding of these two concepts (versioning and the structure to support it) and not be a 100% faithful representation of an application. If you look at them as a blueprint, rather than a conceptual outline, you’ll find a couple SRP violations, etc. Please ignore the nits and focus on the main ideas.

Internal API Diagram

In the beginning, there was an application consisting of a class named Greeter, which had the job of constructing a greeting for some named person from another person. A user interface was created to collect the necessary information from the end-user, pass it to Greeter and display the results. The input to the Greet method is an object of type GreetRequest, which has members identifying the sender and recipient. Greeter.Greet() returns a GreetResponse, the sole member of which is a string containing the Greeting.

And it was good (actually, it was Hello World which until recently was just a cheesy little sample program but is now worth boatloads of cash – should you find yourself using these diagrams to pitch something to a VC, sending me a cut would probably be good karma 😉 ).

At some point, the decision was made to make the core functionality available to external applications (where external is defined as client applications built and deployed separately from the component in question, regardless of whether the team responsible for the client is internal or external to the organization). If the internal API were exposed directly, the ability to change Greeter, GreetRequest and GreetResponse would be severely constrained. Evolving that functionality could easily lead to non-DRY code if backwards compatibility is a concern.

Note: Backwards compatibility is always a concern unless you’re ditching the existing client. The option is synchronized development and deployment which is slightly more painful than trimming your fingernails with a chainsaw – definitely not recommended.

The alternative is to create a facade/adapter class (GreetingsService) along with complementary message classes (GreetingRequest and GreetingResponse) that can serve as the published interface. The GreetingsService exists to receive the GreetingRequest, manage its transformation to a GreetRequest, delegate to Greeter and manage the transformation of the GreetResponse into a GreetingResponse which is returned to the caller (this is an example of the SRP problem I mentioned above, in actual practice, some of those tasks would be handled by other classes external to GreetingsService – an example can be found here).

Internal API with External Service Adapter Diagram

Later, someone decided that the application should have multilingual capability. Wouldn’t it be cool if you could choose between “Hello William, from your friend Gene” and “Hola Guillermo, de su amigo Eugenio”? The question, however, is how to enable this without breaking clients using GreetingsService. The answer is to add the Language property to the GreetRequest (Language being of the GreetingLanguage enumeration type) and making the default value of Language be English. We can now create GreetingsServiceV1 which does everything GreetingsService does (substituting GreetingRequestV1 and GreetingResponseV1 for GreetingRequest and GreetingResponse) and adds the new language capability. The result is like this:

Internal API with External Service Adapters (2 versions) Diagram

Because Language defaults to English, there’s no need to modify GreetingsService at all. It should continue to work as-is and its clients will continue to receive the same results. The same type of results can be obtained using a loose versioning scheme (additions, which should be ignored by existing clients, are okay; you only have to add a new version if the change is something that would break the interface, like a deletion). The “can” and “should” raise flags for me – I have control issues (which is incredibly useful when you support published services).

Control is the best reason for preferring a strict versioning scheme. If, for example, we wanted to change the default language to Spanish going forward while maintaining backward compatibility, we could not do that under a loose regime without introducing a lot of kludgy complexity. With the strict scheme, it would be trivial (just change the default on GreetingRequestV1 to Spanish and you’re done). With the strict scheme I can even retire the GreetingService once GreetingServiceV1 is operational and the old clients have had a chance to migrate to the new version.

Our last illustration is just to reinforce what’s been said above. This time a property has been added to control the number of times the greeting is generated. GreetingsServiceV2 and its messages support that and all prior functionality, while GreetingsService and GreetingsServiceV1 are unchanged.

Internal API with External Service Adapters (3 versions) Diagram

As noted above, being well-defined, stable, internally simple and DRY are all positive attributes for published services. A strict versioning scheme provides those attributes and control over what versions are available.

A Service by Any Other Name

Romeo and Juliet - balcony scene

I remember when reuse was the Holy Grail. First it was object-oriented languages, then object modeling, then components, then services. While none of these has lived up to the promise of rampant reuse, one thing has – language. We have learned to overload and reuse terms to the point that most are no more descriptive than using the word “thing”. “Service” is one of these.

The OASIS Reference Model for Service Oriented Architecture 1.0 defines a service as “…a mechanism to enable access to one or more capabilities, where the access is provided using a prescribed interface and is exercised consistent with constraints and policies as specified by the service description”. This definition includes nothing about protocol, media types, messaging patterns, etc. Essentially, this definition applies to anything that exposes something to consumers over in a defined interface in a defined manner.

Well, that’s helpful. It doesn’t even resolve whether it’s referring to the application providing services or the specific endpoints exposed by that application. In the context of Domain-Driven Design, a service may even be share the same process with its client.

Some services are meant to be interactive. A request is made and a response is returned. Such a service is extremely useful when the response is being presented to an actual human user in response to some action of theirs. This immediate gratification provides an excellent user experience provided latency is minimized. When interactive services are composed within another interactive service, this latency burden quickly increases as the remote client can’t regain control until the service regains it from each remote service it’s called (barring a timeout).

Some services are meant to work asynchronously. These work well particularly well in system to system scenarios when there’s no need to for temporal coupling. A significant trade-off for this model is the additional complexity involved, particularly around error condition feedback.

Some services (typically SOAP-based) are meant to receive and return highly structured and well-defined messages invoking tasks to be performed. This works extremely well for many system to system communications. Others (typically RESTful services) provide more of a variation of media types which are dealt with using CRUD operations. These work particularly well when the client is mainly presenting the response (perhaps with some minimal transformation) to an end user. If the messages are less well-defined, then the level of client complexity increases. Services using well-defined messages that are exposed externally will typically have different versioning requirements than services meant for consumption by internal clients (where “internal” is defined as built and deployed contemporaneously with the service).

The points behind this litany of service definitions and usage patterns? Different styles are appropriate to different situations and precision in communication is important to avoid conflating those styles.

Providing functionality via a service, regardless of the type of service, is not sufficient to meet a need. Functionality should be exposed via the flavor of service that corresponds to the needs of the client. Otherwise, the client may incur more effort and complexity dealing with the mismatch than the functionality is worth.

The concept of a message-oriented API for the back-end allows it to meet all of these needs without violating the DRY principle. Just like UI components, service endpoints (SOAP or REST style) should not contain domain logic, but delegate to domain services. As such, this provides a thin layer primarily concerned with translation between the format and messaging pattern required externally and that required by the domain services. This allows internal consumers (again, where “internal” is defined as built and deployed contemporaneously with the back-end) to work either directly in-process with domain services or remotely via a very thin shim that retains the same message structure and solely provides the network communication capabilities. External consumers should have their own endpoints (perhaps even separate sites) that cater to their needs.

Having the thinnest possible service layer not only prevents duplication of logic but also lessens the burden of standing up new endpoints. Lowering the cost of an endpoint makes it easier to justify providing tailored APIs. APIs that are tailored to specific client types are simpler to consume, further reducing costs.

In my last post, I asked “do you need a service layer?” The answer remains, “it depends”. But now there’s a new wrinkle, “if you need a service layer, does it need to be a remote one?”