If you’ve been around the software community, you’re probably aware of the microservice architectural pattern that has become prevalent in the industry. Every team with a monolithic architecture suddenly wants to migrate to a microservices pattern. There are a few good reasons for this:
- It allows for teams to independently develop and version components of the system
- Deployments of individual components don’t impact the entire system
- Parts of the system that have a heavier load can be scaled independently of the rest of the system
- A single component going down (theoretically) doesn’t bring down your whole system
That final point is what I want to focus on. When breaking down a monolith into microservices, most people aren’t thinking of their dependency graph in the same terms as they might think about it with a monolith.
Dependencies in IoC containers
In many monolithic software architectures, developers take it upon themselves to introduce an “Inversion of Control” container that allows for dependencies to be injected into one another. The container serves as a bag of types, and when you need a type in your system, you simply specify it as a parameter in another type and the container fills it out for you. Different languages and platforms have different models for this, but overall the process is as easy as specifying which “thing” you want, and as if by magic, that “thing” becomes available to you. It also makes it easier to depend on interfaces, allowing the underlying implementation to change while the expectations in the behavior of said thing can stay the same.
This is generally a Very Good Thing™ and you should generally consider doing it for large, unruly systems with many dependencies.
Here is a disgustingly contrived example:
public interface IThingDoer
{
string Process(string input);
}
public class MyThingDoer : IThingDoer
{
public string Process(string input)
{
return input.Replace("Ethan", "Ethan is the best");
}
}
public class System
{
private readonly IThingDoer _thingDoer;
public System(IThingDoer thingDoer)
{
_thingDoer = thingDoer;
}
public void DoTheThing()
{
System.Console.WriteLine(_thingDoer.Process("Ethan"));
}
}
public static void Main(string args)
{
var container = new IOCContainer(); // implementations differ
container.RegisterType<IThingDoer, MyThingDoer>();
container.RegisterType<System>();
container.Resolve<System>().DoTheThing();
}
From the above, you can see that there is absolutely no need to write new MyThingDoer()
anywhere in the code. It is managed by the IOCContainer, and resolved automatically from the container when a System
is requested. An increase in the number of dependencies has no impact on construction of the object, since it is centrally managed elsewhere by the container - you just add them as constructor parameters and it just works.
What does this have to do with microservices?
An IOC container in a monolithic application has a special constraint. An IThingDoer
can no longer have a dependency on a System
, because that would result in a circular dependency, which causes a runtime error. You find this out immediately if you ever try to instantiate a MyThingDoer
that takes a System
as a dependency.
In a microservices environment, many developers and architects alike think of each microservice as a “class” of sorts. This is a Very Not Good Thing™ because microservices should represent much more than a single class; rather each service should represent a library/assembly/package.
One danger of the microservices pattern is creating a system in which services must depend on each other in exactly the way that is prohibited by the IoC container in the first place. ServiceA does some processing, requests info from ServiceB, which calls back to ServiceA for additional information, and eventually responds to ServiceA with its result. Now, the dependency graph goes two ways, which completely nullifies the benefit of having components that break independently of one another.
Does this really happen?
Unfortunately….yes. At least one system I have worked on introduced this pattern. In the real world case, one microservice (ServiceA) was responsible for doing a business logic calculation, and had to reach out to an external server for live data. The external server contained both a baseline and updated values for comparison, which ServiceA would diff and respond with.
However, our design introduced another service, ServiceB, which was assigned for development to a second team. This service became responsible for getting the baseline data and managing it, in addition to providing some other mechanisms that Product Management decided they wanted. In addition, this ServiceB was designed to call ServiceA to get the diff of the baseline and updated values.
The result was that ServiceB would call ServiceA, ServiceA would check the live server for updated information, but would also call back to ServiceB for the baseline information, so it could perform the diff.
Are you confused yet? I certainly am, and I wrote the code to do at least some of this stuff.
On The Importance of Retrospectives
How do we solve this type of problem? I believe the answer is one word: Feedback.
In scrum-oriented organizations, I firmly believe the retrospective is the most important “ceremony” prescribed by the process. Scrum is oriented around the scientific method and adaptability. Retrospectives provide a regular feedback loop for ways that any and all processes can be improved, preventing the organization from being stuck in a rut or processes from becoming stale in a constantly changing environment. They can also be cathartic for when a sprint goes poorly, or some external factor outside of the team’s control is blocking development.
In my scenario, the following problems existed:
- Design and planning were done in parallel with development
- Teams were told how to implement their services
- The responsibilities of each service were decided by architects
- Teams were siloed from each other
I think we can all agree that these factors do not contribute to a successful project. However, they are all dwarfed by the biggest problem of all: we were not doing retrospectives. Without a proper way to surface feedback, we were stuck with the hand we were dealt, which made for a very challenging project out of one that did not need to be nearly so challenging.
Many organizations have latched on to the Agile/Scrum buzzword machine in hopes of attracting talent, while in reality practicing “scrumterfall” (Scrum-in-name-only, or waterfall with stand-ups). This can be detrimental if you’re blindsided by the practice, but also presents a genuine growth opportunity to advocate for better feedback to your organization.