Skip to content

From monolith to microservices: when, why, how

Keyboard: ← → to move, F for full screen, O for overview.

Tan holding a blueprint

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?

Where does the boundary sit?
ApproachBoundary
Classic monolithNo boundary. Everything reaches everything, and over time it becomes a "big ball of mud".
MicroservicesThere is a boundary, but the boundary is also a network call.
Modular monolithThere 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.

app/Modules
app/└── Modules/  ├── Billing/  │   ├── Domain/         # module's entities, value objects  │   ├── Application/    # use cases  │   ├── Infrastructure/ # repositories, external adapters  │   └── BillingApi.php  # the module's ONE public surface  ├── Catalog/  └── 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.
Tan giving a thumbs-up

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 microservicesIn a monolith
CallSerialization, latency, partial failureIn-process, free
ConsistencySaga, outbox, eventual consistencyA single DB transaction
ObservabilityDistributed tracing requiredOne log stream
DeployVersioned, backward-compatible contractsOne deploy
RefactorA contract nobody verifiesThe compiler verifies

Wrong reasons

The wrong medicine for the right disease

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.
ReasonThe 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

  1. Step 1: Pick the modulethe one module giving the signal
  2. Step 2: Build the servicethe new service comes up
  3. Step 3: Shift the trafficcalls are routed gradually
  4. Step 4: Delete the oldthe 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.
Tan waving

Thank you

sade.dev

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

Share, embed, download