From Monolith to Microservices
Decide what to split and how to draw service boundaries.
From Monolith to Microservices is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Monolith to Microservices
Microservices are an organizational and operational bet, not a default. Splitting a PHP monolith trades in-process simplicity for network calls, distributed data, and deployment complexity. Done for the wrong reasons it makes everything slower.
This lesson is about the hardest part: drawing service boundaries. Get the seams right and the rest is plumbing; get them wrong and you build a distributed monolith — all the cost, none of the benefit.
Why (and Why Not) Split
Good reasons to split:
- Independent deployability and team ownership.
- Independent scaling of hot subsystems.
- Technology/runtime isolation and fault containment.
Bad reasons: "the code is messy" (refactor the monolith first), or chasing a trend. If two services must always deploy together or share a database table, they are one service wearing two hats.
Bounded Contexts
Domain-Driven Design gives the sharpest tool for boundaries: the bounded context. Within a context, terms have one precise meaning. "Customer" in Billing (an invoice target) differs from "Customer" in Support (a ticket author).
Each bounded context is a strong candidate for a service. Boundaries follow the business language and how the org actually communicates — Conway's Law in action.
High Cohesion, Low Coupling
A good service boundary keeps things that change together inside, and pushes things that change independently out. Measure a proposed split by:
- How many features require touching two services at once? (Should be few.)
- How chatty is the call pattern between them? (Should be coarse-grained.)
If implementing one feature constantly straddles a boundary, the boundary is in the wrong place.
<?php
// Chatty boundary smell: N network calls to render one view
foreach ($order->lineItems as $item) {
$product = $catalogApi->get($item->productId); // one call PER item!
$names[] = $product['name'];
}
// Coarse-grained: one batch call across the boundary
$ids = array_map(fn($i) => $i->productId, $order->lineItems);
$names = $catalogApi->getMany($ids); // single round-trip
echo count($names) . " products in one call\n";Database per Service
The non-negotiable rule: each service owns its data and no other service touches its tables. Shared databases recreate the coupling you split to escape — a schema change breaks unrelated services.
This means cross-service queries that were a SQL JOIN in the monolith become API calls or replicated read models. That is the price, and it is the point.
<?php
// In the monolith: one JOIN across domains
$sql = 'SELECT o.id, c.email
FROM orders o JOIN customers c ON c.id = o.customer_id';
// After split: Orders service holds only the foreign id;
// it asks the Customers service for the rest (or keeps a local read model).
$order = $orderRepo->find($id); // local
$customer = $customersApi->get($order->customerId); // network call
echo $order->id . ' / ' . $customer->email . "\n";The Strangler Fig Pattern
Never rewrite a monolith big-bang. The strangler fig pattern extracts incrementally: route a slice of traffic to a new service through a facade/proxy, grow it, and retire the old code path only when the new one fully covers it.
A reverse proxy (or API gateway) sits in front and decides, per route, whether to hit the legacy monolith or the new service. Migration proceeds one capability at a time, always shippable.
<?php
// Facade routing: peel off one capability at a time
function route(string $path): string {
$migrated = ['/invoices', '/invoices/pdf']; // moved to billing-svc
foreach ($migrated as $prefix) {
if (str_starts_with($path, $prefix)) {
return 'http://billing-svc' . $path;
}
}
return 'http://legacy-monolith' . $path; // everything else, for now
}
echo route('/invoices/pdf'), "\n";
echo route('/users/42'), "\n";Extracting a Module
A pragmatic extraction order:
- Find a module with few inbound dependencies and clear data ownership.
- Wrap its in-process calls behind an interface in the monolith first.
- Move the data it owns into its own schema/DB.
- Replace the interface implementation with a network client.
- Cut the traffic over via the facade; delete the old code.
Doing the interface-wrapping inside the monolith first de-risks the network step.
<?php
// Step 2: hide the implementation behind a port the monolith calls
interface InvoiceService {
public function generate(string $orderId): string; // returns invoice id
}
// Today: local class. Tomorrow: HTTP client to billing-svc.
// The monolith's calling code never changes.
final class LocalInvoiceService implements InvoiceService {
public function generate(string $orderId): string { return 'inv-1'; }
}Distributed Data Consistency
Once data is split, you lose cross-service ACID transactions. Embrace eventual consistency: services publish events about their own data and others build local read models from those events.
The Orders service doesn't query Customers on every request — it keeps a small projection (just the fields it needs), updated by CustomerUpdated events. This removes a runtime dependency and a latency hop.
<?php
// Orders service keeps a tiny local projection of customer data
function onCustomerUpdated(PDO $db, array $evt): void {
$db->prepare(
'INSERT INTO customer_read_model (id, email)
VALUES (:id, :email)
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email'
)->execute(['id' => $evt['id'], 'email' => $evt['email']]);
}Operational Reality
Microservices shift complexity from code to operations. Before you split, you need:
- Centralized logging and distributed tracing (correlation ids across hops).
- CI/CD per service and independent versioned deploys.
- Health checks, timeouts, retries, and circuit breakers on every call.
- Contract tests so a provider can't silently break a consumer.
If your team can't run one service well, ten will be ten times worse.
Right-Sizing Services
"Micro" is misleading — size services by business capability and team ownership, not lines of code. Too fine-grained (nanoservices) and a single feature fans out into a storm of network calls; too coarse and you're back to a monolith.
A healthy heuristic: a service should be ownable by one team, deployable on its own, and able to fulfill its core use cases without a synchronous chain through many peers.
The Modular Monolith Alternative
Before going distributed, consider the modular monolith: one deployable, but internally split into modules with explicit boundaries and their own schemas, communicating only through published interfaces — no cross-module table access.
You get clean boundaries and easy refactoring without distributed-systems tax. When a module truly needs independent scaling or ownership, it's already shaped to be lifted out via the strangler pattern. For most teams this is the right first stop.
<?php
// Modules talk only through interfaces, never each other's tables.
namespace App\Billing; // owns billing_* tables
interface BillingFacade {
public function invoiceForOrder(string $orderId): string;
}
namespace App\Sales; // owns sales_* tables
final class Checkout {
public function __construct(private \App\Billing\BillingFacade $billing) {}
// Sales never SELECTs from billing_* directly - only via the facade.
}Quick Check
Recognizing a bad split.
Recap
Drawing service boundaries well:
- Split for deployability, scaling, and ownership — not because code is messy.
- Align boundaries to bounded contexts; aim for high cohesion, low coupling.
- Database per service — no shared tables; replace JOINs with APIs or read models.
- Migrate incrementally with the strangler fig pattern.
- Accept eventual consistency and invest in operations before scaling out.
Next: how those services actually talk — REST and gRPC.
Frequently asked questions
Is the “From Monolith to Microservices” lesson free?
Yes — the full text of “From Monolith to Microservices” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “From Monolith to Microservices”?
Decide what to split and how to draw service boundaries. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “From Monolith to Microservices” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- From Monolith to Microservices
- Service Communication: REST and gRPC
- API Gateways and Service Discovery
- Resilience: Circuit Breakers and Retries