Directive: Invert Control
Last updated February 9, 2024
At my first full-time software engineering job, engineering leadership would periodically come out with directives. Each directive would be passed down the management chain until each manager, architect, technical product owner, and engineer was talking about it, and the whole engineering organization would beat that drum for months until it became second nature. The directives were always short and memorable, simple on their face, but had surprising depth. Eg. “buy not build” or “Invert Control”.
These directives were supported by a strong team of senior engineers & architects who took the time to think about the directive and what it meant for their domain and their teams, and then would ask thoughtful and challenging questions to push their teams to think and build software in ways that supported the directives.
This document will discuss the most impactful of those directives, “Invert Control”. When I first heard it, I remember thinking: “What the heck does that mean? I’m building an ecommerce website. Who or what am I supposed to invert control to?”. But I observed over the next couple years that this single directive elevated the software quality of the entire organization. Post-directive, the software and services produced by this organization were more generic, making them flexible and reusable; and more focused, making them more effective at solving the problem they were intended to solve.
What does “Invert Control” Mean?
It’s similar to the inversion of control concept that many engineers are familiar with, but in this case applying it at a larger scope - instead of a framework delegating control to the custom code utilizing that framework, we have services and domains delegating control to configuration files or the services that consume them.
At its core, inverting control is about separating business logic from function. It is another take on the single responsibility principle, in this case separating the requirements of the business from the functionality required to fulfill those requirements.
This directive is simple but brilliant. The specifics of how exactly inverting control should be applied to each service and domain varied, but every domain had something to get out of it. Some domains, in particular those that had no direct exposure to customers, were able to become completely generic functional building blocks for other domains to use, and others found that they needed to invest more in the control layers encoding business logic, composing their own components with other domains to build a useful end product. That was where the senior engineers and architects shined: they interpreted the directive, figuring out how to apply the the directive within the context of their domains and guiding the more junior engineers towards an interpretation of the directive that made sense for their team.
Example: Inverting Control in an Ecommerce Cart Experience
One of the strongest examples of inverting control that I observed was championed by the architect of our cart & checkout domains. The company sold business cards and other printed marketing materials, but he challenged his team to build a cart and checkout experience that could also be used to purchase a spaceship. In other words, make the cart & checkout experience agnostic of the items that were being purchased. The only parts of the item that were required were the product, display name, quantity, and price, with fields like image, product options, and metadata being optional. The ability to modify items in the cart was handled using a plugin pattern so different items could support different modification experiences without the cart needing to have any knowledge of how things were changed. The core cart experience didn’t know anything about how the product and price were associated. The association of how to get the price for a given product was abstracted to a control layer, so different tenants (or even different products) that used the cart could leverage different price providers.
This is a good example that demonstrates how inverting control isolates the essence of what a cart is from the business requirements specific to this particular cart. The core cart service stripped out all business requirements and instead focused on delivering a generic ecommerce cart experience, and delivered an API that is pure to that use case. The requirements specific to this business, including integration with data sources like a product catalog or a pricing service, were handled by external adapter layers. As business requirements changed or services evolved, the cart was isolated from those changes. All that needed to change was the relevant adapter layer, config file, or supporting service that encapsulated relevant business logic. Separating the concerns in this way made the purpose of each component focused and easy to reason about. Additionally, it meant that as new features were requested and added, the teams that owned the cart were much more able to think about the generic case, because their code was already factored that way. If a product needed some validations in cart, they would design a generic way to execute validations against any product and display error messages in the cart appropriately, then create a plugin to run the necessary validation for the product that generated that requirement. It prepared them to handle other similar use cases in the future.
Benefit: Focus on the Domain
Getting engineers, architects, and teams to think about how to invert control leads to prompts them to dig a little deeper to identify the generic problem and separate it from the specific business requirements or the existing domain concepts.
I find this exercise to be quite helpful as it avoids a common pitfall that is conflating domain problems and business requirements. If those two can be separated, it can lead to much cleaner design and much cleaner code because each can be much more focused in what they deal with. It makes it much easier to answer the question my first tech lead asked all the time: “What problem are we trying to solve?”
Benefit: Swappable Components
If teams can successfully invert control, separating the generic domain problems from the business requirements, it can be immensely helpful in identifying the proper domain boundaries for domain-driven design, which in turn can lead to design that enables easily leveraging SaaS or open source offerings off the shelf, freeing the company’s engineers to focus on the things that differentiate the company from its competitors instead of rebuilding something that’s already been built. Even if the team doesn’t choose to leverage open source or a SaaS offering, having clear domain boundaries makes it much easier to rebuild and replace the components in a domain to handle changing business requirements.