Monolithic architectures are a simple and effective way to build software. Over time, competing forces often push and pull architecture in different directions, resulting in monoliths that inhibit development, rather than accelerate it. When your software reaches this point, it’s time to re-architect by breaking the monolith into smaller, more manageable components.
Serverless architecture is an alternative paradigm. Serverless (also known as Functions-as-a-Service or FaaS) breaks features into individual, lightweight functions fully managed, operated, and scaled by the platform. This approach promises to keep components small, light, and composable—all ideal software architecture traits and where monoliths falter. These characteristics offer a path away from monolithic architecture. Yet while all new technologies espouse benefits, experienced engineers know that everything has trade-offs. Re-architecting a monolithic system requires assessing those trade-offs, understanding your organization’s current state, making long-lasting technical decisions, and committing to change.
Break, Bend, or Scrap Your Monolith?
Moving away from a monolithic system begins by diagnosing pain points. While a single reason should not justify a complete re-architecture (with the exception of operating cost), you may want to consider a change if any of the following challenges ring true:
- New work requires touching many different layers.
- Failures in seemingly isolated areas cause failures in other areas.
- Existing code cannot scale to meet current requirements.
- Tests are harder to write.
- It takes new engineers months to understand critical flows.
- Unsustainable operating costs.
- Growing team size outpaces code’s ability to support team’s needs.
If you decide to re-architect your monolith, you have three options: break the system into smaller components, bend (or massage) the system into a more workable state, or scrap and rewrite the system entirely. Scrapping and rewriting your system is not recommended. It’s a complicated approach guaranteed to grow in scope, take longer than expected, and put intense pressure on the development team.
Bending or breaking the system have higher success rates. Bending your monolith entails improving the monolith’s architecture by re-architecting internals. This approach demands careful attention to detail and ongoing focus from all engineers. Although it can be done, re-architecting to promote stronger boundaries in an ever increasing code base only goes so far.
You’ll find more happy engineers who slowly broke down their monolith systems into smaller components. This is because increasing software development performance correlates with decreasing batch sizes. In other words, smaller and more composable architectures create better software. Because smaller components yield smaller bounded contexts, they are easier to build, deploy, and operate. The “move fast and break things” mantra does not apply here. “Move slowly and fix things” does.
Identifying Dividing Lines
Serverless platforms are an ideal candidate for components broken out from a monolith, since Serverless functions fit the Single Responsibility Principle and can be composed into larger applications.
Software architecture is the craft of drawing and enforcing boundaries, and boundaries in a monolith act as layers inside the larger whole. To break a monolith into smaller components, you need to choose where you will split the system. This designated “dividing line” marks where components in your monolith become separate bounded contexts. Selecting proper dividing lines is crucial, and every monolith includes a few potential options. You’ll likely find them around code that changes too often, not at all, or along technical boundaries provided by frameworks or third-party libraries.
Dividing-line candidates have an existing API, which may be a class interface or method call, and are not highly coupled to a data store. Existing consumers may be refactored by dropping in a new implementation class or changing method signatures. Integration points with fewer consumers are easier to refactor and to use to build more refined bounded contexts on the component end.
Separating a monolith into smaller components can be useful in numerous situations, such as splitting a job queue and breaking an API out from a combined API/web application.
Splitting Job Queues
Here’s a common flow. A user creates an account on a web application, which processes the sign up and schedules a job to send a welcome email. Because sending the email during the HTTP request/response cycle is bad practice, it’s saved and processed asynchronously by another thread or process instead. This job is a strong candidate for splitting out of the monolith because the calling code uses an API and the individual actions lend themselves to the single responsibility principle. Common job queue implementations map a job directly to a single class or function, which maps to Serverless architecture.
Existing methods calls can be replaced with Serverless API calls. Another solution pushes messages onto a queue, such as AWS SQS, with an attached Lambda function. Either solution may improve scalability and reduce costs. A pay-per-use function replaces a process running on a VM 24/7.
Mixing a general purpose API, web experience, and mobile or front-end APIs into a monolith begs for trouble. Competing interests diverge, evolve at different rates, and enforce different requirements on lower abstraction layers. In addition, different areas may be owned by different engineering teams, leading to inconsistent code or even bottlenecks in development. If each platform requires a dedicated API, then build one. In addition, different teams can maintain the code with their own preferences.
In both cases, the team writing and building the API can deploy at its own cadence, which promotes client/server API compatibility—compared to a centrally controlled and managed API. Furthermore, teams that are unfamiliar with operations can leverage Serverless platforms, which handle the fundamentals and provide room to grow.
Points to Consider
It’s important to consider your motivating factors when deciding what to break out from the monolith. You can choose dividing lines to curb technical debt, promote component independence, reduce cost, or fit the existing development team. Remember that each split creates another moving infrastructure piece, which needs to be deployed and hooked into metric, logging, and monitoring systems. Each piece also requires attention and maintenance throughout its lifecycle.
Monolith-based teams can grow accustomed to having coalescing responsibilities in a single place. Splitting the monolith means shifting toward distributed systems, which is an entirely different way of working. Teams sometimes become overambitious, splitting their monoliths into far too many pieces. This is a trap. changes in one component require changes in another, and business concepts leak across services. This can be avoided by moving slowly at first and splitting out only when the current situation is untenable.
While there is no specific rule about what is too big or too small to break out of the monolith, you can follow the existing interface and shared data guidelines if you’re unsure. Break the guidelines if absolutely necessary, but do this explicitly and for valuable reasons.
Breaking the monolith is more accessible than it’s ever been, as Serverless platforms are readily available and extremely robust. Splitting a monolith into smaller Serverless functions promotes long-term software growth as well as smaller batches that help teams build, ship, and run software more efficiently.
Today, more and more teams are breaking their monoliths and documenting their experiences, which others can learn from. Remember that changes to your architecture must be made meticulously and to address real problems.
In short, don’t let your monolith collapse under its own weight. Get ahead of it by breaking it into smaller pieces and start shipping.