Vertical Coupling: The Silent Killer
A perennially popular topic in software engineering circles is “monolithic” architectures and how to fix them. Architecture being a bit of a misnomer, since often the problem is the lack of any architecture. The way the story goes is that you have some 10-or-so-year-old web application. Over the years your faithful steed has grown to include so much interrelated functionality that the gears of progress are grinding to a halt with each additional line of code. That single application handles your onboarding, billing, content, messaging system, gamification, third-party integrations, and many additional systems, which are coupled in often insidiously mysterious ways.
There is more than one strategy for dealing with this sort of situation. The most popular is probably despair. Another is microservices, which involves splitting all that functionality into separate, logical applications.
This can be a huge improvement. It can also be an excuse to get bogged down by a ton of technology-creep and research expeditions. What it often isn’t is a change in architecture. It is hard to pull off the microservices gambit successfully without decoupling and cleaning up a ton of code, this is true. Many of the benefits that accrue, however, tend to be from simple refactoring and physically enforced separation of concerns. Often times, the individual services end up looking much the same as the original app, just cut down to size and cleaned up a bit. To be clear: this isn’t a bad thing. Nor is it a universal experience. What it also isn’t, necessarily, is a different architecture.
Perhaps I’m splitting hairs, here. If someone wants to talk about reducing horizontal integration as an architectural concern, that’s perfectly fine—the end result is still better software. Where I wish there were greater attention given is the architectural problems of vertically integrated code, and how to resolve them. I actually have to give a tip of the hat to Rails, here, because this actually was recognized as a problem, fairly early on. “Skinny controllers, fat models” is a good example of a “best practice” that evolved out of an effort to reduce vertical coupling in the application stack. Of course, those “fat models” are a problem all their own.
What do I mean by vertical coupling? Well, I work mostly with APIs on the server-side of things, often with the Grape framework. It is incredibly common to see—and not just in a project using Grape—the following concerns combined into a single class: routing/transport, conditional authorization, business logic, database access, serialization, and data shaping. There are probably others. These are all things your app needs, but by collapsing them to a single point the complexity can explode enormously.
Imagine a set of finite state machines—each one can be delightfully simple and together they can be combined in useful and efficient ways. If, however, you decide to take three state machines that are always used in conjunction and just smoosh them together into a single state machine, you’re almost guaranteed to create a huge, unmaintainable mess with a massive increase in the number of states and transitions. That’s what happens when you combine all those concerns together in one place: the number of states that code represents jumps enormously, making it harder to reason about, harder to change, harder to test, and harder to have confidence in.
What we want to do, ideally, is separate at least some of those concerns and move them behind architectural boundaries.
For example, Grape is great at describing the shape of your external HTTP API—your routes, verbs, and parameters. Forget for a second that some users shouldn’t be able to access every route. There should be, in principle, a sort of API platonic ideal that just describes every possible way HTTP can be used to interact with your app. You should also be able to describe that form without hardcoding in your ORM classes or business logic. Of course, a bunch of HTTP endpoints by themselves is pretty useless. The thing is, the fact that you access your app via HTTP isn’t really an integral aspect of your app. It might be an unchanging aspect, but what your app does is what your app does whether it’s accessed via HTTP, websocket-based messaging, a command line, or some hypothetical future protocol.
It’s right there in the name: Application Programming Interface. An interface is an abstraction. An interface is a mechanism for separating concerns while ensuring they can continue to be composed and used together. There are actually two interfaces at play, here. There’s the super obvious one where you’re defining endpoints that a browser or app can hit. There’s also the more pure interface of your application business logic and its underlying domain. Squashing the two together is a mistake, because it robs you of the key advantage of the second kind of interface: the ability to vary its implementation.
Why would you want two implementations of your application? You don’t. The thing to remember is that we’re talking about two implementations of an interface, not two implementations of an application—the larger part of your application should itself be encapsulated appropriately. The actual concrete implementations of its public interface(s) should be only a small part of your total application code.
OK, why would we want two implementations of the interface, then? Well, one reason could be to provide one implementation that is limited in functionality, for unauthenticated or unprivileged users, and a second implementation that implements the entire interface. Now, access control can depend only on which implementation gets injected into the external, web-facing interface, in a single location, rather than sprinkling potentially brittle and conflicting authorization guards throughout the HTTP API. Think of how much easier it would be to reason about and test your access control when you take HTTP routing and requests out of the picture entirely.
This is obviously a light sketch of one possibility, but hopefully the advantages are clear. Sometime soon I’ll have a second post, about the practicalities of following this approach with Grape, along with concrete examples.