Get updates to your emailSubscribe
It doesn't matter if your service is "micro" or "oriented", if it's tightly coupled – especially if your service is on the Web – you're going to be stuck nursing your service (and all it's consumer apps) through lots of pain every time each little change happens (e.g. addresses, operations, arguments, process-flow). And that's just needless pain. Needless for you and for anyone attempting to consume it.
Tight Coupling is Trouble Tight coupling to any external component or service – what i call a "fatal dependency" – is big trouble. You don't want it. Run away! How do you know if you have a fatal dependency? If some service or component you use changes and your code breaks – that's fatal. It doesn't matter what code framework, software pattern or architectural style you are using – breakage is fatal. Stop it!
- Have an alternative service provider (or set of them)
- Write your code such that the unavailable dependency doesn't mean your code is essentially unusable ("Sorry, our bank is unable to perform deposits today")
And the circuit breaker pattern is not meant for use when services introduce breaking changes anyway – it's for cases when the dependent service is temporarily unavailable.
A Promise You're much better off using services that promise their consumers that any changes to that service will be non-breaking. In other words, changes to the interface will be only additive. No existing operations, arguments or process-flows will be taken away. This is not really hard to do – except that existing tooling (code editors, build-tools and testing platforms) make it really easy break that promise!
Interface Tooling is Weak There are lots of refactoring tools that make it hard to break existing code but not many focus on making it hard to break existing public interfaces. And it's rare to see testing tools that go "red" when a public interface changes even though they are great at catching changes in private function signatures. Bummer.
So, you want to use services that keep the "no breaking changes" pledge, right? That means you also want to deploy services that make that pledge, too.
Honoring the Pledge But how do you honor this "no breaking changes" pledge and still update your service with new features and bug fixes? Turns out that isn't very difficult – it just takes some discipline.
Here is a quick checklist for implementing the pledge:
- Promise operations, not addresses
Service providers should promise to support a named operation (
findCustomer) instead of promising exact addresses for those operations (
http://myservice.example.org/findCustomer). On the Web, you can do that using properties like
idthat have predetermined values that are well-documented. When this happens, clients can "memorize" the name instead of the address.
- Promise message formats, not object serializations Object models are bound to change – and change often for new services. Trying to get all your service consumers to learn and track your object model changes is just plain wrong. And, even if you wanted all consumers to keep up with your team's model changes, that means your feature velocity is tied to the slowest consumer in your ecosystem - blech! Instead, promise generic message formats that don't require an understanding of object models. Formats like VoiceXML and Collection+JSON are specifically designed to support this kind of promise. HTML, Atom and some other formats can be used in a way that maintains this promise, too. Clients can now "bind" for the message format, not the object model – now changes to the model on the service don't leak out to the consumers. When this happens, adding new data elements in the response or re-arranging the service's internal object model will not break clients.
- Promise transitions, not functions
Service providers should treat all public interface operations as message-based transitions, not fixed functions with arguments. That means you need to give up on the classic RPC-style implementation patterns so many tools lead you into. Instead, publish operations that pass messages (using registered formats like
application/x-form-urlencoded), which contain the arguments currently needed for that operation. When this happens, clients only need to "memorize" the argument names (all predefined in well-written documentation) and then pay attention to the transition details that are supplied in service responses. Some "old school" people call these transition details FORMs but it doesn't matter what you call them as long as you promise to use them.
- Promise dynamic process-flows, not static execution chains
Services should not promise fixed-path workflows ("I promise you will always execute steps X then A, then Q, then F, then be done"). This leads consumers into hard-coding that nonsense into their apps and those consumers will break whenever you want to modify the workflow due to new business processes. Instead, services should promise operation identifiers (see above) along with a limited set of process-flow identifiers (
done) that work with any process-flow that you need to support. When this happens, clients only need to "memorize" the small set of generic (static) process-flow keywords and can be coded to act accordingly.
Not Complicated, Just Hard Work You'll note that all four of the above promises are not complicated – and certainly not complex. But they do represent some hard work. It's a bummer that tooling doesn't make these kinds of promises easy. In fact, most tools do the opposite. They make creating services that depend on address-based, object-serialization with fixed argument functions and static execution chains easy – in some tools, these are the defaults and you only need to press "build and deploy" to get it all working. BAM!
So, yeah, this job is not so easy. That's why you need to be diligent and disciplined for this kind of work.
Eliminating Dependencies And – back to the original point here – decoupling addresses, operations, arguments and process-flow means you eliminate lots of fatal dependencies in your system. It is now safer to make changes in components without so much worry about unexpected side-effects. And this will be a big deal for all you microservice fans out there because deploying dozens of independent services explodes your interface-to-operation ratio and it's just brutal to do that with tightly-coupled interfaces that fail to support these promises inherent in a loosely-coupled implementation.
For the Win So, do not fear. Whether you are a "microservice" lover or a "service-oriented" fan, you'll do fine as long as your make and keep these four promises. And, if you're a consumer of services, you now have some clear measures on whether the service you are about to "bind" to will result in fatalities in your system.
(This post was previously published on my personal blog.)
An internationally-known author and lecturer, Mike Amundsen travels throughout the United States and Europe, consulting and speaking on a wide range of topics including distributed network architecture, Web application development and cloud computing. His recent work focuses on the role hypermedia plays in creating and maintaining applications that can successfully evolve over time. He has more than a dozen books to his credit, the most recent of which is RESTful Web APIs.
The lack of common concepts and axioms is holding the software engineering industry back. This blog post explores the need for a common distributed systems vocabulary to help with that problem.
Matt McLarty on Aug 10, 2018