Services in the Monolith

Services in the Monolith

It's become accepted that microservices, when done right, are a Good Thing - helping you keep your responsibilities nicely separated, easily share functionality between applications, and scale in a granular fashion where only the hot spots are spread across a multitude of servers. It's less commonly accepted, but even more true, that jumping in and trying to go microservices-first carries a high chance of unmitigated disaster, leaving you with so much spaghetti strewn across disparately located pieces of your network, incurring enormous quantities of latency throwing messages between services that bear no resemblance to the problem domain.

A good compromise is to start by building services in the monolith.

This is where you start out by building your application as an old-school single project, but designing it with a view that parts of it may become separate services in future. Which means extremely loose coupling, and carefully designing your interfaces to minimise the amount of "chat" that goes on. The end result should be a number of highly cohesive modules, which will typically interact via a CQRS-like pattern where each module only sees a single call to a GetTheThings() or MakeTheStuffHappen() method, rather than a prolonged transaction that hits half a dozen methods to get its job done.

The key is that while this is the end result, it's not necessarily the starting point or even most of the intermediate ones. This is what makes services in the monolith such a powerful approach. Because you're only communicating over memory in the same process to start off with, it's easy to refactor, because you're not having to redefine service boundaries every time you move things between modules. You don't have to worry up-front whether something can be async fire-and-forget or not, you can wait until you actually know rather than trying to guess.

What you're doing is waiting for services to emerge naturally, rather than trying to guess them up-front; deferring the decision until you have more knowledge. As long as you consciously try to keep things loosely coupled and independent, and refactor eagerly with this goal in mind, the service boundaries will become obvious - they're the points where there's only one or two method calls, to a cohesive set of classes with a clear responsibility. Essentially, the flow is:

  • Start with a monolith
  • Refactor continuously with the goal of loose coupling and indendence
  • Split candidate services out to libraries, modules, Nuget packages etc. first
  • Turn libraries into services where this provides a useful benefit

That last step carries an important qualifier; you don't want to turn something into a service unless the monitoring, latency and support overheads are worth it. This is usually when it has its own databases or external dependencies that need to be taken care of, where it needs to scale independently of the rest of the application, or it needs to be used across multiple different technology stacks. Ask the question, "is this better as a service or a library?" If something genuinely works better as a library that you can pull in with npm or Nuget, then leave it as a library - don't be tempted by microservice envy.

Caveats: it's easier to move around and refactor services within a monolith, but it's also easier to turn it into spaghetti with no clear service boundaries. Sometimes you have to compromise on what you consider good practice. Writing clean code in the "real world" is often a trade-off between multiple ideas of best practice; for services in the monolith, you need to favour cohesive modules and minimal communication across responsibility boundaries even if it comes at the expense of other guidelines. You'll often end up ignoring much of what's been written about layered architectures in favour of a leaner, CQRS-inspired model to reduce interdependence between different parts of the application.

The benefit is you don't spend the first part of a project setting up hosting and monitoring for ten different services, none of which you understand the responsibility of that well. You don't spend the last part of a project locked into a service architecture that doesn't really reflect the domain model as you understand it after a few months of work, but unwilling to refactor it because it involves tearing up large chunks of your build, deployment and monitoring infrastructure. Instead, you start with something that is simple and easy to get running on a server somewhere (great for your fast feedback) and over time spin out the services which are actually useful and make sense.

Image modified from original by Ricardo Liberato | CC BY-SA 2.0