From monolith to microservices: when, why, how
Keyboard: ← → to move, F for full screen, O for overview.

Muhammet Şafak — Presentations
From monolith to microservices
When, why, how: draw the boundary today, split the deployment once a measured signal arrives.
Muhammet Şafak
Section 01
Boundary
Modular monolith: one codebase, one deploy, one database; clear module boundaries inside.
Three approaches
Where does the boundary sit?
| Approach | Boundary |
|---|---|
| Classic monolith | No boundary. Everything reaches everything, and over time it becomes a "big ball of mud". |
| Microservices | There is a boundary, but the boundary is also a network call. |
| Modular monolith | There is a boundary, but it lives inside the process; it is a method call. |
The real issue
Boundary, service: two decisions.
Draw the boundary today, split the deployment when it is really needed.
The boundary in code
One public surface per module
A module calls only the *Api.php class of another module. It never runs a SELECT directly against another module’s table and never ties a foreign key to it.
1app/2└── Modules/3 ├── Billing/4 │ ├── Domain/ # module's entities, value objects5 │ ├── Application/ # use cases6 │ ├── Infrastructure/ # repositories, external adapters7 │ └── BillingApi.php # the module's ONE public surface8 ├── Catalog/9 └── Notification/Why I start here
Five concrete reasons
- Cheap to fixA wrongly drawn boundary is fixed with one refactor.
- One transactionTwo modules update consistently inside a single DB transaction.
- Less operationsOne deploy, one log stream, one thing to monitor.
- Refactoring toolsThe compiler verifies a method call.
- Cheap migrationExtracting a module with a clear boundary into a service is mechanical work.

Discipline
Four practices that keep the boundary standing
- The dependency rule runs in CI; a violation breaks the build (deptrac) (done)
- Each module's public API is a single class (done)
- Relations between modules are an ID + a public call, not a foreign key (done)
- Review question: does this change cross a module boundary without permission? (done)
All the value is in the discipline
No CI check, no boundary.
If the boundary isn't enforced by CI, what you have isn't a modular monolith, just a monolith with tidy folder names.
Section 02
Cost
Microservices are not a solution, they are a trade-off.
One change
A method call turns into a network call.
What you get is independent deployment and independent scaling. What you pay in return is long.
Trade-off
The microservices bill
| In microservices | In a monolith | |
|---|---|---|
| Call | Serialization, latency, partial failure | In-process, free |
| Consistency | Saga, outbox, eventual consistency | A single DB transaction |
| Observability | Distributed tracing required | One log stream |
| Deploy | Versioned, backward-compatible contracts | One deploy |
| Refactor | A contract nobody verifies | The compiler verifies |
Wrong reasons
The wrong medicine for the right disease
| Reason | The real fix |
|---|---|
| "The codebase is too big." | Module boundaries, file layout, dead code cleanup |
| "Deploys are slow and scary." | Fast pipeline, tests, gradual rollout, rollback |
| "I need to scale independently." | Monoliths scale out too: N copies + load balancer |
| "This is modern architecture." | Modern is not an architectural reason |
| "Netflix does it this way." | Not the problem, only the operational load gets copied |
Code size
Same code, plus a network.
A 200-thousand-line monolith and 200 thousand lines split into 20 services are the same amount of code; the second one just has a network in between.
Section 03
Signal
At least one of these signals must have been measured.
Measured signals
Four signals that justify the move
- Scaling profileOne part consumes resources very differently from the rest.
- OwnershipTeams wait on each other's deploys; the boundary is organizational.
- RuntimeA measured bottleneck: the workload is clearly cheaper in Go or Rust.
- Fault isolationOne module's crash must not take the system down; it can't be achieved in-process.
Threshold
Not on a graph? Not a signal.
If a signal doesn't show up on a graph, it is not yet a signal.
Section 04
Migration
Not everything: split out the single module that gives the signal.
Strangler pattern
Taking one module out
- 1Step 1: Pick the module
the one module giving the signal - 2Step 2: Build the service
the new service comes up - 3Step 3: Shift the traffic
calls are routed gradually - 4Step 4: Delete the old
the old code is deleted
When, and when not
When to split, and how not to
- Big-bangSplitting everything at once is the most expensive migration and the one that fails most often.
- TeamWhen several teams are squeezed into the same deploy unit.
- ResourceWhen one part's resource profile holds the rest of the system hostage.
- CrashWhen one component's crash regularly takes down the whole system.
Boring architecture
The complexity budget is limited
- PHP + Laravelor Symfony; the application layer.
- PostgreSQLRelational database.
- RedisIn-memory data store.
- NginxWeb server.
- LinuxProcess management with systemd + Supervisor.
- New toolMy threshold for boring: 12–18 months of active use in production.

Thank you
Draw the boundaries early, deploy late. Wait for the signal; let measurement make the decision, not fashion.