A modular monolith can take you surprisingly far.
You can have clear business boundaries, independent modules, explicit contracts, good tests, background workers, queues, caching, horizontal scaling and strong deployment automation without splitting the application into separate services.
So at what point should one of those modules actually become a microservice?
I don't think the answer is:
“When the application gets big.”
And it definitely isn't:
“When we reach enough users.”
The better question is:
What problem would independent deployment solve that the modular monolith can no longer solve well?
That distinction matters.
Start with a real boundary
Suppose an application contains:
Customers
Orders
Billing
Reporting
NotificationsInside a modular monolith, these can already be separate architectural units.
They may have their own services, domain rules, tests and public contracts while still running inside one application.
That means:
Module boundary ≠ Service boundaryA module says:
“This capability owns this responsibility.”
A service adds another decision:
“This capability should also run and deploy independently.”
That second decision comes with significant cost.
So there should be evidence for it.
Signal 1: Independent scaling
Imagine Reporting becomes dramatically more expensive than the rest of the application.
A few large customers begin generating millions of records and running heavy analytics.
Now you might have:
Customers normal load
Orders normal load
Billing normal load
Reporting 20x workloadScaling the entire application just to handle Reporting may become wasteful.
If Reporting already has a clean boundary, moving it into a separately scalable service starts to make sense.
This is a good extraction signal because there is a measurable problem.
Signal 2: Independent deployment becomes valuable
Suppose Billing must change frequently because payment providers, tax rules or compliance requirements keep changing.
But every Billing deployment currently requires releasing the entire application.
That may eventually create unnecessary coordination.
If Billing has:
a clear API,
clear data ownership,
good tests,
independent operational requirements,
then independent deployment may become valuable.
Again, the reason is not “microservices are better.”
The reason is that deployment coupling has become expensive.
Signal 3: Team ownership changes
Architecture often follows organizations.
When five developers work on the same application, a single deployment model may be perfectly manageable.
When several teams independently own Customers, Billing, Reporting and Notifications, coordination pressure increases.
At some point, this:
Team A ─┐
Team B ─┼── one deployment
Team C ─┘may become a bottleneck.
A service boundary can allow a team to own its capability more independently.
But I would still resist splitting purely because teams exist.
The business boundary should already be healthy before distribution makes it stronger.
Otherwise you simply turn internal coupling into network coupling.
Signal 4: Security or compliance needs stronger isolation
Sometimes a capability genuinely needs a stronger boundary.
Payments may need different access controls.
Sensitive documents may require stricter infrastructure.
A regulated workload may require separate audit, deployment or operational policies.
Now process or network isolation may provide something the in-process module cannot.
This is one of the stronger reasons to extract because the boundary is driven by an actual security or compliance requirement.
Signal 5: The runtime requirements are genuinely different
Maybe most of your platform works perfectly well in PHP.
But one capability eventually needs something very different.
Perhaps a worker needs extreme concurrency.
Maybe image processing is CPU-heavy.
Perhaps another ecosystem provides a substantially better tool for a specific workload.
With a good modular architecture, you might eventually have:
PHP Application
├── Customers
├── Orders
├── Billing
└── Reporting
↓
Specialized Processing Service
Go / Rust / PHPThe important part is that the extraction is local.
You don't rewrite the application because one workload changed.
What is not a strong reason?
I would be cautious about extracting because:
"The codebase is getting large."
"We might scale one day."
"Big companies use microservices."
"We want Kubernetes."
"Services look cleaner."Those may describe future possibilities.
They do not necessarily describe current problems.
Microservices introduce their own architecture:
network failures,
retries,
idempotency,
authentication between services,
distributed observability,
deployment coordination,
message/version compatibility,
data consistency,
operational overhead.
Before extraction, most of those problems may not exist.
After extraction, they become your responsibility.
Data ownership is the hardest test
Before I extract a module, I would ask one particularly uncomfortable question:
Can this capability genuinely own its data?
Suppose Billing is extracted but still directly reads and writes twenty tables owned by Orders and Customers.
You haven't really created an independent service.
You have created a distributed application sharing a database.
Sometimes that is an acceptable intermediate step.
But it should be recognized as one.
A stronger boundary looks more like:
Orders owns order state
Billing owns payment state
Orders → Billing contract
Billing → Orders contractOwnership becomes explicit.
Extraction should be earned
The progression I prefer is:
Monolith
↓
Modular Monolith
↓
Observe real pressure
↓
Identify one boundary
↓
Prove independent ownership
↓
Extract if the benefit exceeds the costThat gives the architecture time to tell you where distribution is actually useful.
It also means some modules may never become services.
That is completely fine.
A successful modular monolith does not have to “graduate”
This is probably the biggest misconception.
A modular monolith is not necessarily an intermediate architecture waiting to become microservices.
It may be the correct long-term architecture.
And if one capability eventually deserves independent deployment, you should be able to extract that capability, not redesign the entire system.
That is the architectural direction I care about with EvolvePHP as well:
start modular, preserve boundaries, measure pressure, and extract selectively.
The question should never be:
“Are we big enough for microservices?”
It should be:
“What concrete problem becomes easier if this particular boundary becomes independently deployable?”
If there isn't a strong answer yet, the module can probably stay exactly where it is.

No comments:
Post a Comment