Practical guide
How to split a monolithic application into modules
A monolith does not have to be a problem. Split it where changes truly hurt, one safe step at a time.
First, the short version
A module is not a microservice
A monolith is one deployable application. It can still have good internal boundaries. A module is a part of code that keeps one business whole together, such as orders or inventory.
The goal is not to create as many folders as possible or move to microservices immediately. The goal is to reduce coupling: a change in inventory should not break invoicing merely because they accidentally share internal details.
Get ready
What you need before the first move
First find out where the application hurts most. That is usually a better start than drawing modules from database tables.
- One frequently changing area, such as orders, catalogue, or invoicing.
- A list of places usually changed with it: controllers, services, entities, templates, and integration code.
- At least basic automated tests around the chosen area. Without them, you will not know whether a move broke anything.
- An agreement that a module has public entry points and other parts will not reach into its internal classes.
Steps 1 to 3
Split the monolith in small cuts
Move by feature, not by file type. The goal is a coherent unit that can do its own work.
1. Find the first bounded context
- Choose an area with its own language and rules. Orders, products, and invoices are often better boundaries than “controllers” and “entities”.
- Write what belongs to the area and what does not. An order can ask for payment, for example, but should not know a payment gateway’s internal tables.
- Start with one module. Five new modules without clear responsibility only move confusion into new folders.
mkdir -p src/Order Official Symfony recommendations for code organisation 2. Create a small public module entry point
- Write down what the rest of the application may ask the module for: for example, PlaceOrder or CancelOrder. These are its public use cases.
- A controller or another module must not change module entities directly. It calls a public use case and receives a result.
- Keep data between modules small. Pass IDs, values, and results instead of an entire object graph through the application.
3. Move one feature with its rules
- Choose one concrete use case. Move its application service, domain rules, and tests together.
- Keep the old call temporarily as a thin transition layer. Remove it only after callers have moved over.
- Put every dependency leaving the module behind an interface. The module then does not get tied to a specific database, email service, or external SDK.
php bin/phpunit --testsuite=Order Official Symfony autowiring documentation Step 4
Check that the module holds together
A good first module makes the next change easier. It does not need to be perfect or completely isolated from everything else.
-
Run tests for the chosen area
First check only the feature you moved. Then run the complete test suite.
php bin/phpunit -
Search for direct access into the module
Inspect imports from other application parts. They should not depend on a module’s internal classes unless that is an explicitly agreed exception.
rg "App\\Order\\" src -
Make a small rule change
For example, change the condition under which an order can be cancelled. You should mostly edit the module and its tests, not random files across the application.
If something goes wrong
Common problems
A new module exists, but everything still calls old services
Move one specific caller to the module’s public use case first. Then another. Without gradual redirection, the new module remains only a code copy.
Modules share database entities across their boundary
Stop passing around the entire entity. Give the other module an ID or a small data shape. If it needs more, consider its own query or a public application entry point.
The split stopped feature development
Make the cut smaller. The goal is not to clean an entire module in a week, but to safely move one valuable feature while normal changes continue.
Every module has a copy of the same rule
Before extracting a shared library, check whether it truly is the same rule. Similar code can have different business meaning; premature sharing creates more coupling.
Done
The monolith has its first clear boundary.
Add more modules only when there is a real reason. One understandable boundary is more valuable than ten folders with new names.