Back to Blog

Why Break It If It Works? The Case for Modular Monoliths

Nakul Shukla (Updated: )
Why Break It If It Works? The Case for Modular Monoliths

Modern application development is often described as a balancing act — balancing speed, scalability, and simplicity. Over the years, we’ve transitioned from monoliths to micro-services, often in pursuit of agility and faster releases. However, the journey is not without its pitfalls. I’ve seen teams (and, let’s be honest, I’ve been part of some) dive headfirst into microservices, only to find themselves entangled in distributed chaos.

Microservices promise a lot: independent deployments, scalability, and faster time to market. But they also bring challenges:

  • Operational complexity: Managing inter-service communication and distributed systems can quickly escalate.
  • Premature granularity: Splitting services before fully understanding domain boundaries often leads to rework.
  • Cost: Both in infrastructure and developer time.

Having worked on both sides of the architectural spectrum, I’ve realized that the key decision isn’t whether to start with microservices or a monolith — it’s to think modular from the very beginning. This mindset lays the groundwork for flexibility and scalability. By respecting core principles of module division — clear boundaries, independence, and loose coupling — you create a system that can grow and adapt naturally. It’s a more fundamental approach that simplifies architecture without overcommitting to either extreme.

Mistakes developers make with Microservices

Let me be frank - diving into micro-services without a plan is like trying to assemble IKEA furniture without the manual: you’ll probably end up with something functional, but not without a lot of frustration and extra screws. Or maybe it’s like deciding to juggle flaming torches before mastering a tennis ball - impressive if it works, but more likely to end in disaster. Okay, enough with the analogies; I think you get the point. Here are some of the most common mistakes I’ve seen teams make when rushing into microservices.

  • Too granular, too soon: Splitting services before fully understanding domain boundaries. Result? Distributed chaos.
  • Over-engineering the infrastructure: Spending months setting up Kafka, Redis, and Kubernetes for an app that barely has users.
  • Ignoring team and process readiness: Microservices work best when teams are independent and mature. But if you still need a meeting to decide who writes a unit test, well…

The modular monolith lets you avoid all this while keeping an eye on the future. Think of it as the pragmatic architect’s safety net.

A Modular-Monolith

The term modular monolith is not new. It has been widely discussed in the software community and supported by frameworks such as Service Weaver (by Google) and Spring Modulith (an extension of Spring Boot for modular architecture). These frameworks provide tools and best practices for designing modular systems that can evolve into distributed architectures if needed.

While frameworks can be helpful for supporting modular monoliths, I believe we don’t always need them to achieve this architectural style. Instead, it’s about following a few core principles. Now, I know terms like loose coupling, dependency injection, and replaceable dependencies may sound like architectural buzzwords, but trust me — they have solid reasoning behind them. Let me explain why they are crucial for making a modular monolith work.

  1. Loose Coupling: Each module should know as little as possible about other modules. Why? Because tightly coupled modules become a maintenance nightmare. If one module changes, others can easily break. Loose coupling means that modules interact through well-defined interfaces and contracts, making changes isolated and safer. Think of modules as neighbours. They shouldn’t rely on each other’s furniture arrangements to live peacefully!

  2. Dependency Injection (DI): Swap out implementations without changing the module’s core logic. Why? Because modularity thrives when you can replace components easily. In monolith mode, your modules might use direct Java calls. When you deploy as microservices, those same calls need to turn into HTTP requests. DI makes this possible by injecting the correct implementation based on the environment.

  3. Replaceable Dependencies: Design modules to work with different implementations of services. Why? Because this future-proofs your system. When you eventually split modules into micro-services, you’ll want them to communicate over HTTP or messaging. If modules are too dependent on internal calls, you’ll face rewrites. By keeping dependencies modular and replaceable, your system adapts without needing a total overhaul.

Understanding the Modular Monolith with an Example

Share this post

Stay Updated

Subscribe to my newsletter to receive updates on new blog posts and tech insights.