Pleniko Blog
Modular Monolith or Microservices: What Is the Practical Choice for Business Software?
- Software Architecture
- Modular Monolith
- Microservices
- Business Software
- Internal Systems
- Symfony
- Scalability
- System Design
AuthorPlamen Nikolov
When a new internal business system is planned, one of the first architectural questions is whether the application should be built as a monolith or as a collection of microservices. In technology discussions, microservices are often presented as the natural direction for every modern platform. In practice, however, a more complex architecture does not automatically make a product faster, more reliable or easier to develop.
For many ERP and CRM platforms, administration panels, customer portals and specialised business systems, a well-structured modular monolith is the more practical starting architecture. It keeps business areas clearly separated without forcing the system to carry the cost of distributed infrastructure from its first day.
The important question is not which architecture sounds more modern, but which one matches the actual processes, team, workload and plans for growth.
What is a traditional monolith?
In a monolithic application, the main features are developed, deployed and executed as one system. Users, customers, orders, invoices, inventory, reports and settings may share one application and one database.
This has practical advantages: one deployment, a simpler local environment, direct calls between components and straightforward transactions. Problems arise when the code has no clear boundaries. Every feature then begins to depend on everything else, changes become risky, and testing and maintenance become increasingly difficult.
The core weakness is therefore not necessarily the single application. It is the absence of architectural discipline inside it.
What is a modular monolith?
A modular monolith is also deployed as one application, but its code is organised around separate business domains. A platform might contain modules for:
Each module owns its business logic and exposes a clear way for other parts of the system to work with it. One module should not arbitrarily modify another module's internal data. Interactions take place through agreed interfaces, commands, events, or application services.
The application remains easy to deploy while internally behaving as a collection of clearly separated components.
For many ERP and CRM platforms, administration panels, customer portals and specialised business systems, a well-structured modular monolith is the more practical starting architecture. It keeps business areas clearly separated without forcing the system to carry the cost of distributed infrastructure from its first day.
The important question is not which architecture sounds more modern, but which one matches the actual processes, team, workload and plans for growth.
What is a traditional monolith?
In a monolithic application, the main features are developed, deployed and executed as one system. Users, customers, orders, invoices, inventory, reports and settings may share one application and one database.
This has practical advantages: one deployment, a simpler local environment, direct calls between components and straightforward transactions. Problems arise when the code has no clear boundaries. Every feature then begins to depend on everything else, changes become risky, and testing and maintenance become increasingly difficult.
The core weakness is therefore not necessarily the single application. It is the absence of architectural discipline inside it.
What is a modular monolith?
A modular monolith is also deployed as one application, but its code is organised around separate business domains. A platform might contain modules for:
- users and access control;
- customers and communication;
- quotations and contracts;
- orders and payments;
- inventory and deliveries;
- documents and notifications;
- reporting and analytics.
Each module owns its business logic and exposes a clear way for other parts of the system to work with it. One module should not arbitrarily modify another module's internal data. Interactions take place through agreed interfaces, commands, events, or application services.
The application remains easy to deploy while internally behaving as a collection of clearly separated components.
Why are microservices not automatically the better solution?
A microservice architecture divides a product into independently deployed services. Each service may have its own database, release cycle and scaling strategy. This is a powerful model when an organisation genuinely needs that independence.
The separation also introduces additional work:
- network communication instead of direct calls;
- handling temporary failures, retries and duplicate messages;
- tracing one request across several services;
- versioning APIs and events;
- coordinating data and business operations across databases;
- managing more environments, deployments, logs and metrics;
- more complex integration and end-to-end testing;
- a greater need for mature DevOps and observability.
Each individual service may appear simpler while the complete system becomes more complex. For a small or medium-sized team, that often means spending more time on infrastructure and coordination instead of features that create business value.
Lower operational complexity
A modular monolith usually has one primary application, one consistent configuration and one release process. This reduces the number of components that must be monitored and coordinated.
Local development is also more accessible. A developer can run the system without maintaining many services, versions and network dependencies. Diagnosis is more direct because an operation can be followed inside one process.
This simplicity does not mean the application cannot scale. It can run multiple instances behind a load balancer, use dedicated workers for background tasks, Redis for caching and queues, and separate file storage. The architecture can grow considerably before service separation becomes a real requirement.
Clearer transactions and data consistency
Internal systems often perform operations that must either complete fully or be cancelled: creating an order, reserving stock, issuing a document and recording a payment.
When related data is managed within one application and one transactional database, maintaining consistency is relatively straightforward. Across separate services, the same process may require asynchronous messages, compensating actions and acceptance of temporary inconsistencies.
This does not make distributed operations wrong. It means they should be introduced only when their benefits justify the additional failure modes and more complex business behaviour.
Modularity requires real boundaries
Placing classes in different directories is not enough. A genuinely modular architecture needs clear rules:
- modules are defined by business domains, not only by technical layers;
- public interfaces are small and predictable;
- internal classes are not used directly by other modules;
- data ownership is explicit;
- dependencies follow a controlled direction;
- important interactions are covered by automated tests;
- architectural rules are checked during development.
These boundaries provide value immediately and prepare the system for future extraction. If a module has no clear independence inside the monolith, moving it to another server will not magically create it.
How does Symfony support a modular architecture?
Symfony provides a strong foundation for substantial business applications through dependency injection, clearly configured services, events, validation, security and well-maintained components.
Internal business logic can be organised with PHP namespaces and directories aligned with business domains. This is generally more appropriate than creating a separate Symfony bundle for every internal module, because bundles are primarily intended for functionality that can be reused independently across applications.
Symfony Messenger allows commands and events to be handled synchronously or through a queue. Heavy operations such as document generation, imports, notifications and synchronisation can therefore be delegated to workers without turning the entire system into a microservice platform.
This provides valuable flexibility: start with a simpler architecture while preserving clear boundaries and asynchronous mechanisms wherever they deliver a specific benefit.
A React interface does not determine the backend architecture
An interactive React SPA can work equally well with a modular Symfony backend or with several microservices. The user sees one coherent application while the frontend communicates through a well-designed API layer.
The choice between React, SSR, and PWA should therefore not be confused with the choice between a monolith and microservices. They are separate architectural decisions. For an internal system, React can provide a fast and convenient interface while a modular backend keeps business logic consistent and manageable.
When do microservices provide a real advantage?
Separation is justified when there are specific reasons, such as:
- one module has a significantly different workload and must scale independently;
- several autonomous teams release at different speeds;
- a feature has special security or isolation requirements;
- part of the system objectively requires a different technology;
- failure in one process must not affect the core application;
- the service is shared by more than one product;
- the business boundary is stable and well understood.
User numbers alone are not a sufficient argument. Performance can sometimes be improved more effectively through query optimisation, caching, background processing, stronger infrastructure or horizontal scaling of the existing application.
How can a system move gradually towards separate services?
A modular monolith is not a dead end. When its boundaries are well designed, a particular module can be extracted gradually:
- Measure the real problem and select the appropriate module.
- Define its inputs, outputs and data ownership.
- Introduce a stable contract through an API or events.
- Route external dependencies through that contract.
- Deploy and monitor the module independently.
- Remove the old internal implementation after a safe migration.
The system then evolves according to actual workload without a large and risky rewrite. Only the component with a genuine reason for independence is extracted.
How do we choose an architecture at Pleniko?
At Pleniko, we begin with business processes rather than a predetermined architectural fashion. We analyse modules, roles and permissions, data volume, critical operations, integrations, expected workload and the way the team will develop the product.
For many internal business systems, we build a Symfony backend with clearly separated business modules, PostgreSQL for dependable data storage, Redis and queues where appropriate, and a React interface for efficient daily work. Automated testing, monitoring, backups, and security are part of the architecture rather than additions made after launch.
When measurements show that a specific domain needs independent scaling or deployment, it can be extracted. The objective is not to operate the greatest possible number of services. It is to achieve the least unnecessary complexity while maintaining boundaries that support future growth.
The practical choice is the one that leaves room for growth
Microservices are a valuable architectural tool, but they are not the mandatory starting point for every modern product. For many business applications, a modular monolith offers a better balance between development speed, reliability, data control, and maintenance cost.
Well-designed modules keep the system understandable as its features increase. When a proven need eventually appears, the same boundaries make the gradual extraction of individual services easier.
The quality of an architecture is not measured by the number of servers. It is measured by how safely and predictably it helps the business operate and grow.




