Monolithic systems are often seen as outdated systems and bad practice when thinking about building scalable systems nowadays. In this article we will see why these arguments are false, how to get the best out of monolithic architecture and how it can be a springboard for implementing microservices in your application in the future.
What it is
At its core, a monolithic system encapsulates an “all-in-one” architecture, where all components of an application are interconnected and deployed as a single unit.
Myths about monolithic architecture
Monolithic systems have their limitations and challenges, but many of the criticisms leveled against them are based on false assumptions or outdated perceptions:
Monolithic Systems Are Outdated: Despite misconceptions, monolithic architectures remain relevant in modern software development, with many successful applications still utilizing them (Shopify is an example).
Monolithic Systems Do Not Scale: While scaling monolithic systems poses challenges compared to microservices, they can handle significant loads with proper design and optimization.
Monolithic Systems Impede Business Growth: Contrary to claims, monolithic architectures can provide stability and support iterative development, especially beneficial for startups or rapidly evolving businesses.
Monolithic Systems Have High Coupling: Critics often exaggerate tight coupling issues, which can be mitigated through proper design principles and practices (modular monoliths).
Monolithic Systems Cannot Support Microservices Architecture: Monolithic systems can be refactored into microservices incrementally, allowing for a gradual transition without sacrificing stability.
Different types of monolithic systems
Single-Process Monoliths: These monolithic systems are characterized by a singular focus, where all components of the application reside within a single process. While offering simplicity in design, they can suffer from high coupling between modules.
Distributed Monoliths: Distributed monoliths extend the single-process model by distributing components across multiple servers or nodes while maintaining a monolithic architecture. This approach aims to alleviate scalability concerns but may introduce additional complexity in deployment and maintenance.
Black Box Monoliths: In a black box monolithic architecture, the internal workings of the system are opaque, resembling a closed system where interactions are limited to predefined interfaces. While providing encapsulation and abstraction, black box monoliths may create challenges in understanding and debugging.
Each type of monolithic system comes with its own set of advantages and challenges, according to different requirements and contexts in software development.
When monolithic architecture should be used?
Whenever you want to achieve simplicity, stability, and a unified deployment model you should start with monolithic architecture, but here are some specific situations where using monolithic systems may be beneficial:
Unclear Business Model: In the early stages of a project or when the business model is still evolving, monolithic architectures provide a straightforward development approach. They allow teams to focus on building core functionality without the added complexity of distributed systems.
Core Business Instability: When the core functionalities of an application are subject to frequent changes or uncertainty, a monolithic architecture can provide stability. It simplifies development and maintenance efforts, enabling teams to iterate on features efficiently amidst evolving business requirements.
Simplicity in Deployment: Monolithic systems streamline the deployment process by consolidating all components into a single unit. This simplicity is advantageous in environments where rapid deployment and iteration are crucial, such as small-scale projects or startups.
Operational Simplicity: Organizations with limited resources or operational expertise may prefer monolithic architectures for their simplicity in management and operation. With a single codebase and deployment unit, monitoring, scaling, and troubleshooting become more straightforward tasks.
Transitioning to Microservices: In cases where a future transition to microservices architecture is anticipated, starting with a monolithic system can serve as a pragmatic approach. It allows teams to validate business logic, refine domain boundaries, and gain insights into performance bottlenecks before embarking on a microservices journey. But, in order to do it properly, it’s important to implement a modular monolith.
Strategies to reduce coupling
In order to reduce coupling when building monoliths it’s a good practice to break down the monolithic application into smaller, more manageable modules based on functional boundaries or domain concepts. Each module should encapsulate related functionality and shouldn’t depend on other modules, reducing overall coupling. To achieve this kind of modularity we can apply the following concepts:
Domain-Driven Design (DDD): Adopting DDD principles helps identify and define bounded contexts within the system. By emphasizing domain modeling and ubiquitous language, we can create cohesive and loosely coupled modules, each dealing with a single bounded context.
Communication through Contracts and Facades: Implementing facades will define a clear communication protocol between the modules, encapsulating complex interactions and providing the possibility to completely replace a module in the future without affecting the other modules (Dependency Inversion Principle).
Selective Duplication of Entities: Entities shared across multiple modules can lead to tight coupling. In order to avoid inter-module dependency, in this case we can duplicate entities, making them point to the same database tables and share the same IDs, but having the flexibility to name them according to the context (users, clients, beneficiary, people, etc.) and use only the necessary attributes in each module.
Specialized Teams or Ownership: Assign dedicated teams or ownership responsibilities for individual modules. By having specialized teams focused on specific modules, development efforts become more focused, and dependencies between teams can be minimized, reducing coupling.
High Cohesion: Aim for high cohesion within modules, ensuring that related functionalities are grouped together. Modules with high cohesion tend to have fewer external dependencies and are easier to understand, maintain, and evolve.
Key benefits of modular monoliths
Overall, modular monolithic systems strike a balance between modularity and simplicity, offering many of the benefits associated with microservices architectures while maintaining the operational ease of traditional monolithic systems.
Unified Deployment: Modular monolithic systems allow for a single deployment unit, simplifying the deployment process instead of managing multiple components separately.
Unified Operation: Similarly, the operational aspects of a modular monolithic system are streamlined. Monitoring, scaling, and managing the system can be done holistically, as there is only one operational entity to oversee.
Simplified Observability: With all components of the system tightly integrated, observability becomes more straightforward. Monitoring, logging, and debugging are centralized, making it easier to identify and troubleshoot issues across the entire system. This simplification accelerates problem resolution and improves system reliability.
Lower Latency and Reduced Network Overhead: In a modular monolithic architecture, communication between modules typically occurs within the same process or server, leading to lower latency and reduced network overhead compared to distributed systems. This can result in faster response times and improved performance, especially in latency-sensitive applications.
Consistency in Technology Stack and Governance: Since all components of a modular monolithic system are developed and maintained within the same codebase, there is consistency in the technology stack and development practices. This coherence simplifies governance, reduces compatibility issues, and promotes collaboration among development teams.
Easier Development and Testing: Developers working on a modular monolithic system can have a clear understanding of the entire system architecture and can easily navigate between modules. This facilitates collaborative development and testing, as changes in one module can be quickly validated against the entire system.
While studying this topic I’ve created this repository. It contains a monolith with 6 modules and a small shared kernel between them. These modules were developed with TypeScript, using DDD, and they communicate with each other using facades.
In summary, monolithic systems, often underestimated in today’s tech landscape, offer pragmatic solutions for various development scenarios. By dispelling myths and exploring different types of monolithic architectures, we uncover their strengths in providing stability and simplicity. With strategic approaches to reducing coupling and embracing modularity, monolithic systems can serve as effective platforms for transitioning to microservices. Harnessing their potential enables teams to build scalable, innovative, and resilient applications while meeting the evolving business needs.
Top comments (0)