When it comes to software, communication between different systems can sometimes be tricky to manage. Not simple ping/pong interactions, of course — those are trivial enough — but a complex mesh of calls between many different services. A web of interactions, such as those often found in a microservice architecture, can become brittle rather swiftly.
During the early days of the web, Jon Postel — one of the fathers of the TCP specification — coined the robustness principle as a design guideline to help build resilient interactions.
“… be conservative in what you do, be liberal in what you accept from others …” — Postel’s Law
In real terms, this suggests that, as the recipient of a request, you should be lenient when handling the message. Assuming there is enough information to do your job, who cares if the format is slightly malformed. This could take the form of unexpected properties, additional whitespace from a login form or missing some other non-critical piece of the puzzle.
Conversely, for any request sent, you should adhere to the spec as strictly as you can. Don’t rely on your upstream partners being quite as easygoing as you aim to be.
I find the best way to think about Postel’s is as a set of filters.
In an uncontrolled chain of interactions, the error in one input exacerbates the set of possibilities in the next. Each system needs to be stricter in its output than its input, or else errors can compound as they progress along the call chain. This can lead to a chaotic system.
In applying Postel’s law, each link in the chain acts as a filter for errors. By catching and handling less-than-perfect messages as we go, we reduce the possible variance of errors upstream. This can help us build a more robust system overall.
The tolerant reader pattern is a simple but effective strategy to help apply Postel’s law. On de-serialization of a message, some libraries are extremely strict — if they meet something unexpected, they fail. A tolerant reader would be resilient to unexpected input and carry on as if nothing had happened. This grants the provider the ability to evolve independently of the consumer, as long as they don’t break the original contract.
Domain Driven Design presents us with the idea of an anti-corruption layer. Given two sub-systems, A and B, that communicate but don’t necessarily share the same semantics, how do we keep them well encapsulated and unpolluted?
By introducing an anti-corruption layer — a facade or adapter —between the two, we broker communication between A and B. By not allowing them to talk directly, we are able to prevent corruption of both and to more clearly ensure message compatibility.
This post was written as part of a series on laws of software development for #PragProWriMo 2021 run by the The Pragmatic Programmers.