DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

Monolith vs. Modular: Which of the 3 Architectures is Right for You?

When starting a new project or scaling an existing system, the question "how should we design the architecture?" has always bothered me. It's a decision that directly impacts the months, even years, to come. A wrong choice at the beginning can lead to months of refactoring, team conflicts, and endless deployment issues down the line.

In my experience, architectural choice isn't just a technical matter; it's also directly related to team size, budget, project scope, and even company culture. In this post, I'll examine monolithic, modular monolith, and microservices architectures from my perspective, with real-world experience. My goal is to offer you a practical viewpoint on which architecture might be more suitable for you.

Monolithic Architecture: Simplicity and Quick Start

Monolithic architecture is the traditional approach where the entire application is combined within a single codebase and a single deployable unit. It's a simple and fast-starting structure that I initially preferred for most projects. It can be incredibly efficient, especially with small teams or when developing an MVP (Minimum Viable Product).

Its advantages include development simplicity, easy debugging, and a single deployment process. Since all the code is in one place, navigating the IDE and understanding dependencies is easier. I've always started most of my side projects as monoliths because I needed to launch quickly and validate ideas.

ℹ️ Monolithic Architecture: Quick Start

For a small internal tool at a manufacturing company, I combined order tracking and inventory management into a single FastAPI application. We used PostgreSQL as the database and wrote a simple frontend with Vue.js. The first version of this project was launched in 2.5 months with 3 engineers. The entire system could be brought up with a single docker-compose up command, which allowed us to get quick feedback.

However, as the project grows, the monolithic structure begins to strain. As the codebase expands, compilation times lengthen, and conflicts increase as different teams work on the same code. Even a small change in one module can require redeploying the entire application, increasing risks. I remember managing the backend of an e-commerce site where I had to wait 45 minutes for the CI/CD pipeline to run for a small API change in a codebase exceeding 500,000 lines. This was a serious bottleneck for a team aiming for 5-6 deployments per day.

Modular Monolith: Controlled Growth and Separation

A modular monolith is an approach where, despite being a monolithic structure, the codebase consists of logically separated modules. Each module contains coherent functionality within itself and communicates with other modules through defined interfaces. This allows us more flexibility in managing complexity while retaining the advantages of a monolithic structure.

I've typically chosen this approach as a transition phase for growing monoliths or for projects where I couldn't justify the operational overhead of microservices. In a manufacturing ERP, I designed modules like purchasing, production planning, and shipping as separate Python packages. Each package contained its own ORM models, business logic, and API endpoints, but all ran under the umbrella of the same FastAPI application.

💡 Modular Monolith: Defining Boundaries

In the manufacturing ERP, I ensured that the production_planning module used internal APIs provided by the inventory module instead of direct database access. For example, production planning would call a function like inventory.get_stock_level(item_id) to query the stock of a component. This reduced tight coupling between modules, giving us the flexibility to extract the inventory module into a separate service later. This approach can be further strengthened with [related: Event-Sourcing and CQRS approaches].

A modular monolith facilitates parallel work on different modules by the team and makes the code more readable and maintainable. However, maintaining this modularity requires great discipline. It's crucial for developers not to violate module boundaries and to use APIs instead of direct database access. Otherwise, there's a risk of it turning into "distributed spaghetti code" over time. This can further increase complexity in the long run and fail to deliver the expected benefits.

Microservices Architecture: The Cost of Scalability and Flexibility

Microservices architecture is a structure where an application consists of independent, small services, each with its own area of responsibility. Each service can have its own database, use a different technology stack, and be deployed independently. While this sounds great in theory, it brings significant operational complexities in practice.

My microservices experiences have generally been with very high-traffic systems or in special cases requiring different technology needs. For example, on an internal banking platform, we designed a financial transaction validation service and a customer notification service as separate microservices. The transaction validation service received thousands of requests per second, while the notification service operated at a lower intensity and had different scaling needs. The independent scalability of these services allowed us to use resources much more efficiently.

⚠️ Microservices: A High-Cost Learning Curve

When transitioning to microservices, I personally experienced how complex issues like distributed transaction management, service discovery, centralized logging, and monitoring can be. In one client project, a structure we started with 12 microservices placed a significant burden on the team in the first 6 months due to constant inter-service communication issues and debugging difficulties. Even correlating logs from all services for debugging was a task in itself. This situation showed me how crucial my knowledge in [related: Observability and monitoring strategies] was.

The biggest advantages of microservices are independent scalability and technological flexibility. Each team can choose the most suitable technology for its service. However, this flexibility comes at a cost: increased network latency, data consistency issues, operational overhead, and debugging challenges inherent in distributed systems. Separate deployment pipelines, monitoring systems, and security policies must be created for each service. I usually thoroughly evaluate whether an existing monolithic structure truly presents a bottleneck before starting a project with microservices.

Factors Influencing Architectural Choice: My Checklist

When making an architectural choice, I lay out several key factors. These factors act as a kind of checklist, guiding me toward the right path. Having seen the cost of wrong decisions countless times, I make sure not to skip these steps.

First, team size and experience are critical for me. If I'm working with a small, newly formed team of 3-5 people, I'd choose a monolithic or modular monolith structure rather than grappling with the complexity of microservices. An experienced and larger team can better handle the operational overhead of microservices. Second, project domain complexity and growth expectations are important. If the project's scope and future growth aren't clear, starting with a simple monolith and gradually moving to a modular monolith is smarter.

Factor Monolithic Architecture Modular Monolith Microservices Architecture
Team Size Small (1-5 people) Medium (5-15 people) Large (15+ people)
Initial Cost Very Low Low High
Operational Overhead Low Medium Very High
Scalability Low Medium High
Technological Flexibility Low Medium High
Deployment Speed High High High per Service

In a client project, we were working with a small startup team, and the budget was very tight. Although they initially eagerly wanted to go for a microservices architecture, I convinced them to opt for a modular monolith. We designed the frontend, backend, and data processing layers as separate modules, but all within a single Python project. This way, we kept operational costs to a minimum, launched the product within the first 6 months, and reached 5000 active users. If we had started with microservices, we would likely have exhausted the entire budget within the first 3 months due to distributed system issues.

My Mistakes and Lessons Learned: Lessons from the Past

Throughout my career, I've made both good and bad architectural choices. There were times when I rushed into things with the "microservices are cool" mindset, and I paid a heavy price. Once, while developing the backend for a new side product, despite the very simple business logic, I started with 3 different microservices. These services were brought up with Docker Compose, and each had its own PostgreSQL database.

What was the result? Bringing up 3 services for a simple CRUD operation not only slowed down the local development environment but also, with a separate CI/CD pipeline, a separate monitoring configuration for each service, I started "managing infrastructure" more than doing the actual work. While it seemed cool at first, the project's development time extended by 30% due to this unnecessary complexity. Looking back, it would have been much more sensible to start as a simple modular monolith and separate services only when truly needed. Last month, I realized that 2 of the 3 services were so rarely used that I had to merge them back into a single service.

🔥 The Cost of a Wrong Microservices Choice

On an internal banking platform, we chose a microservices architecture for a non-critical reporting service. The service ran only a few times a day but had its own database, its own deployment pipeline, and its own isolation rules. When we conducted a 6-month operational cost analysis, we found that the maintenance and management of this service far outweighed the benefits it provided. It incurred an additional annual cost of $12,000 USD. Ultimately, we integrated this service as a module within a larger monolithic reporting application.

One of the biggest lessons I've learned is that architectural decisions are not irreversible. Starting with a simpler structure and evolving it according to the project and team's needs is the healthiest approach. Experience the problem first, then implement the solution. Over-engineering for the future with a "just in case" mentality is often a waste of effort. I've written about [related: The Pitfalls of Early Optimization in Software] before, and the same philosophy applies to architectural choices.

Future Approaches: Hybrid and Evolutionary Architecture

Software architecture is not static; it's like a living, breathing organism. As projects grow, teams develop, and business needs change, the architecture must also evolve. In my experience, there's no such thing as "one right architecture." Often, hybrid approaches, combining different architectures, yield the best results.

In a hybrid architecture, the core and relatively stable parts of the application might remain a modular monolith, while critical services requiring high scalability or a different technology stack can be separated as microservices. For example, in a manufacturing ERP, the main workflow (order, production, shipping) could remain within a modular monolith, while performance-critical parts like an AI-powered production planning algorithm or a real-time operator screen could be developed as separate microservices. This allows each component to be optimized according to its specific needs.

ℹ️ Evolutionary Architecture: Step-by-Step Transformation

On an e-commerce platform, we initially had a completely monolithic structure. As order processing volume increased, payment processing and inventory update services began to create bottlenecks. We first separated the payment service from our monolith and turned it into a separate microservice. Then, we also separated inventory management. We completed this transition in about 8 months, transforming only one critical service at a time. This allowed us to minimize risk and successfully transform by gradually acclimating the team to new operational models.

In the future, I believe such evolutionary and hybrid approaches will become even more widespread. Technology is changing rapidly, and new tools and platforms are emerging. In this dynamic environment, an architecture that is flexible and adaptable, rather than adhering to a rigid template, is vital for project sustainability. Technologies like serverless or Function-as-a-Service (FaaS) offer the potential to further shrink some microservices and reduce management overhead. I'm considering migrating some of my small helper services running on my own VPS to FaaS in this way.

Conclusion: The Right Architecture, At the Right Time

Choosing a software architecture is not a one-time decision but a continuous process of balance and adaptation. Monolithic, modular monolith, and microservices architectures each have their own advantages and disadvantages. The important thing is to make the most suitable decision based on the project's current state, the team's competencies, and future goals. In my experience, starting with a simple modular monolith and gradually evolving towards microservices as needs arise has been the healthiest path.

Remember, architectural choice is not just a technical matter; it's a strategic decision that affects your business processes, team structure, and even your company's growth rate. A wrong architecture increases technical debt, slows down development, and can demotivate even the best engineers. Therefore, when making this decision, lay out all the factors, understand the trade-offs well, and adopt a flexible approach where you can say, "that's good enough." I hope this perspective guides you in your next project.

Top comments (0)