A lot of architecture discussions eventually become arguments about deployment models.
Monolith or microservices?
One database or many?
Synchronous or event-driven?
PHP or Go?
Containers or serverless?
Those are useful questions.
But I think they often arrive too early.
The more important question is:
How easily can this system change when the business changes?
Because distribution is only one possible response to change.
And sometimes it is the wrong one.
Distribution is not the same as flexibility
A system can have twenty services and still be tightly coupled.
Imagine:
Orders Service
↓
Billing Service
↓
Customer Service
↓
Inventory Service
↓
Notification ServiceEvery request crosses several network boundaries.
Every service depends on the response shape of another.
Several services still share the same database.
Deployments need careful coordination.
Changing one business workflow requires touching five repositories.
Technically, the system is distributed.
But is it flexible?
Not necessarily.
You can distribute code without distributing responsibility.
That is why I think architecture should optimize first for change, not distribution.
Start with business boundaries
Suppose an application contains:
Customers
Orders
Billing
Reporting
NotificationsBefore asking whether these should become services, I would ask:
- Who owns each capability?
- What data does it own?
- What can it change independently?
- Which dependencies are necessary?
- Which dependencies exist only because the code grew that way?
- What happens if this capability needs a different deployment model later?
Those questions matter whether everything runs inside one process or across ten services.
Good boundaries are useful in both architectures.
A modular monolith can already provide independence
Inside one deployable application, you can still create strong separation.
For example:
Application
│
├── Customers
│
├── Orders
│
├── Billing
│
├── Reporting
│
└── NotificationsEach module can have:
- explicit responsibilities,
- clear dependencies,
- owned data,
- internal implementation details,
- public contracts,
- independent tests.
That gives you something extremely valuable:
locality.
Calls are cheap.
Transactions are simpler.
Debugging is easier.
Deployment is simpler.
Yet the architecture still has boundaries.
That is often a better starting point than immediately introducing a network.
The network should solve a problem
Moving a module into another service adds real costs.
Now you need to think about:
Timeouts
Retries
Authentication
Authorization
Idempotency
Service discovery
Version compatibility
Distributed tracing
Partial failure
Eventual consistency
Deployment coordinationThose costs may absolutely be worth paying.
But there should be a reason.
Maybe Billing needs stronger compliance isolation.
Maybe Reporting needs independent scaling.
Maybe a workload requires a different runtime.
Maybe one team needs independent deployment.
Maybe one capability has become operationally expensive enough to separate.
Those are real pressures.
“Microservices are more scalable” is not enough by itself.
Design for extraction before extracting
One principle I like is:
Make something extractable before deciding to extract it.
That changes the architecture conversation.
Instead of immediately creating a Billing service, first make Billing a healthy module.
Give it:
Clear inputs
Clear outputs
Clear data ownership
Explicit dependencies
Independent tests
Minimal access to unrelated internalsThen observe it.
Maybe it never needs to leave the monolith.
Good.
You avoided unnecessary complexity.
But if three years later Billing needs independent deployment, the architecture has already done much of the preparation.
That is architectural optionality.
Avoid pretending everything is replaceable
There is another trap.
Once people start designing for change, they sometimes create abstractions around everything.
Every database call gets an interface.
Every library gets a wrapper.
Every framework feature gets hidden behind another layer.
Every function becomes “replaceable.”
That can become its own form of complexity.
Not every dependency needs to be replaceable.
Not every implementation needs three abstraction layers.
The goal is not maximum flexibility.
The goal is strategic flexibility.
Protect the things likely to create expensive coupling:
- business boundaries,
- data ownership,
- external integrations,
- execution state,
- infrastructure dependencies,
- deployment boundaries.
Let simple things remain simple.
Runtime choice should also remain a local decision
This applies to programming languages too.
Suppose 95% of a platform works perfectly well in PHP.
But one capability eventually needs extreme concurrency or specialized computation.
A poorly structured architecture creates this conversation:
“Maybe we should rewrite the application in Go.”
A healthier architecture creates a different one:
“Does this one capability need a different runtime?”
That might lead to:
PHP Application
├── Customers
├── Orders
├── Billing
└── Admin
Specialized Processing Service
↓
GoThe rest of the application remains where it is.
That is a much smaller decision.
Architecture should allow local changes to remain local.
The same applies to infrastructure
Maybe you begin with:
One PHP application
One relational database
One queueLater:
Reporting gets a read replicaLater:
Search gets its own indexLater:
Media processing gets dedicated workersLater:
Billing becomes independently deployedThe architecture evolved because specific pressures appeared.
You did not need to predict all of them on day one.
You only needed to avoid making them unnecessarily difficult.
Change has many forms
When I say “build for change,” I do not only mean microservices.
Change might mean:
Upgrade PHP
Replace a library
Change a database
Introduce a queue
Add multi-tenancy
Support a new deployment model
Adopt persistent workers
Move one capability
Integrate another system
Replace part of the frameworkA system designed only around today's deployment model can struggle with all of these.
A system designed around explicit responsibilities has more room to move.
This is the architectural direction behind EvolvePHP
A major goal I have for EvolvePHP is not:
“Help everyone build microservices.”
It is closer to:
“Help applications preserve useful choices.”
That is why the framework direction emphasizes things like:
- modular boundaries,
- explicit contracts,
- infrastructure separation,
- execution lifetimes,
- interoperability,
- incremental modernization,
- Bridge-based coexistence,
- observability,
- selective extraction.
The ideal outcome is not a particular deployment diagram.
It is an application that can evolve without needing a full architectural reset every time its environment changes.
Sometimes the monolith should stay a monolith
This is worth saying clearly.
If an application works well as a modular monolith for ten years, that is not an architectural failure.
It may be the correct architecture.
A module does not need to become a service to prove that the architecture worked.
In fact, one sign of a good architecture may be that you could extract something but never needed to.
You preserved the option without paying the operational cost.
Architecture should preserve decisions, not make them early
I think the best architecture does not answer every future question today.
It makes future questions easier to answer.
Can this capability scale independently?
Can its data ownership change?
Can we move it to another process?
Can we replace this integration?
Can we upgrade the runtime?
Can we modernize one part without rewriting everything?
If the answer is generally yes, then the architecture has preserved something valuable:
choice.
So I would not start by asking:
“Should this system be distributed?”
I would ask:
“What choices might we reasonably need later, and are today's decisions making those choices unnecessarily expensive?”
Build boundaries first.
Measure pressure.
Distribute only where distribution earns its cost.
Because the goal is not to build for microservices.
The goal is to build for change.

No comments:
Post a Comment