
Have you ever felt that your project is too large for a monolith, but too small for the operational complexity of microservices? Perhaps you’ve been forced to split your code into multiple repositories just to scale a specific part of the system.
In modern software development, there is a strategic middle ground: Profile-Driven Modular Architecture. This approach allows the deployment topology to be a simple runtime configuration decision, eliminating unnecessary technical friction.
To demonstrate this, we will explore ModularMS, a project that exemplifies how this model allows you to transition between different architectures without rewriting a single line of business logic.
Normally, the scalability path is rigid: you start with a monolith and "evolve" into microservices through a massive rewrite. However, this step often brings the "distributed complexity tax."
With the ModularMS proposal, modules are the primary design unit, and deployment is just an option. The same code can adapt to three different scenarios:
┌────────────────────────────────────┐
│ Application │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Module A│ │Module B│ │Module C│ │
│ └────────┘ └────────┘ └────────┘ │
└────────────────────────────────────┘
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Module A │ │ Module B │ │ Module C │
│ Service │ │ Service │ │ Service │
└────────────┘ └────────────┘ └────────────┘
┌──────────────────┐ ┌──────────────────┐
│ Service X │ │ Service Y │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │ Module A │ │ │ │ Module C │ │
│ │ Module B │ │ │ │ Module D │ │
│ └────────────┘ │ │ └────────────┘ │
└──────────────────┘ └──────────────────┘
For a modular architecture to be effective, boundaries must be real and verifiable. Tools like Spring Modulith integrate with IDEs like IntelliJ IDEA to offer visual validation of encapsulation. (Image 4)
While working in the development environment, you can quickly identify the status of your packages through icons: closed padlocks indicate protected internal packages, while open ones represent public APIs. If Module A tries to access an internal class from Module B, the system will immediately flag the error in red.
The magic behind ModularMS lies in the combination of defined boundaries and the strategic use of @Profile and @ComponentScan in Spring Boot.
Each module manages its own logic and communicates via events to maintain decoupling. This allows communication to be transparent, whether local or distributed.
@Externalized("new-order.#{#this.productId()}")
public record OrderPlacedEvent(String productId, int quantity) { }
Instead of heavy global configurations, we use profiles to activate only what is necessary. By running a specific profile command, the system only loads the required modules, allowing everything from a single microservice to a functional monolith for local testing.
@Configuration
@Profile("orders")
@ComponentScan(basePackages = "me.lbenavides.modularms.order")
public class OrdersModuleConfig { }
Adopting a modular deployment is both a technical decision and an improvement in software delivery strategy:
From my perspective, architecture should grow with the team, not ahead of it. With ModularMS, my goal is to demonstrate that we can maintain clean boundaries and impeccable documentation without paying the "distributed complexity tax" from day one.
Ultimately, it’s about choosing tools that allow us to focus on what really matters: delivering value agilely, without infrastructure becoming an obstacle. If you want to see how to implement this in your own projects, I invite you to explore the repository—and of course, any feedback is more than welcome!