Development

Ditching the Monolith: A Pragmatic Path to Scalable Systems

Ditching the Monolith: A Pragmatic Path to Scalable Systems

A monolith is not a failure. It is often the fastest way to turn a useful idea into a working product: one repository, one deployment, one database, and a straightforward mental model. The trouble begins when the system’s growth outpaces the team’s ability to change it safely.

At that point, “move to microservices” can sound like an obvious answer. It is not. Splitting a codebase introduces network failures, distributed data, harder debugging, more deployments, and new operational responsibilities. The pragmatic goal is not to abandon the monolith on principle. It is to reduce the cost and risk of change while preserving reliability.

Recognize the real reasons to split

A large application can remain healthy for a long time. File count, repository size, or team enthusiasm for Kubernetes are weak reasons to decompose it. Look instead for persistent boundaries that create tangible delivery or scaling problems.

  • A subsystem needs independent scaling, such as image processing, report generation, or webhook delivery.
  • Changes in one area repeatedly force risky releases across unrelated areas.
  • A domain has different availability, security, or data-retention requirements.
  • Teams cannot work independently because ownership and release coordination are constantly entangled.
  • A workload is harming the responsiveness of a customer-facing request path.

These signals point to a boundary with operational value. A vague desire to “modernize” does not. If the primary pain is a tangled codebase, begin by improving its internal structure. A well-modularized monolith is often the best platform from which to extract services later.

Make the monolith modular before extracting anything

In a PHP backend, that usually means organizing code around business capabilities rather than technical layers alone. Instead of allowing every controller, command, and model to reach into every table and namespace, establish modules such as billing, identity, catalog, or fulfillment. Each module should expose deliberate application-level operations and keep its persistence details private.

For example, an order module may provide an operation that places an order and emits a domain event. Other modules should not update the order tables directly just because it is convenient. This discipline is valuable even if everything still runs in one PHP application and one database.

final class PlaceOrder
{
    public function __construct(
        private OrderRepository $orders,
        private EventPublisher $events,
    ) {
    }

    public function handle(PlaceOrderRequest $request): Order
    {
        $order = Order::create($request->customerId, $request->items);

        $this->orders->save($order);
        $this->events->publish(new OrderPlaced($order->id()));

        return $order;
    }
}

The example is intentionally simple. In production, consider transaction boundaries, validation, idempotency, and how events are persisted. The important point is that the module owns the workflow. That ownership becomes the contract for a future service, rather than an accidental collection of tables.

Extract a capability, not a technical layer

Good first candidates are usually narrow, asynchronous, or resource-intensive capabilities: search indexing, notifications, document conversion, audit export, or payment-provider integration. They have clear inputs and outputs, and their failures can often be retried without blocking a user request.

A poor first extraction is the core business workflow that touches every part of the system. That may eventually be necessary, but it magnifies every distributed-systems problem at once: data ownership, transaction consistency, latency, authentication, versioning, and incident response.

Define the contract before the deployment

Start with the interaction, not the framework. Decide whether the new component needs a synchronous API, an asynchronous message, or both. A request-response API is appropriate when the caller needs an immediate answer. A queue is usually better when work can happen later and should survive temporary failures.

For external or cross-service APIs, make failure behavior explicit. Timeouts, retries, duplicate requests, and partial outages are normal conditions. A payment request, for example, should carry an idempotency key so a retry does not create a second charge. Retrying every error blindly is not resilience; it can turn an outage into a flood of duplicate work.

$response = $client->post('/payments', [
    'headers' => [
        'Idempotency-Key' => $idempotencyKey,
    ],
    'json' => $payload,
    'timeout' => 3.0,
]);

A timeout only tells the caller that no response arrived in time. It does not prove that the remote system did nothing. Design the API and reconciliation process around that uncertainty.

Move data ownership carefully

The most common architectural shortcut is also one of the most damaging: several services reading and writing the same database tables. It feels efficient because no data needs to move, but it preserves the old coupling while hiding it behind separate deployments.

Each extracted service should become the authoritative owner of its data. Other services communicate through APIs or events, and maintain their own read models where needed. This may introduce eventual consistency, which is not automatically a defect. It simply requires product-aware design: show a pending state, provide clear status updates, and avoid promising immediate results when the workflow is asynchronous.

Migration should be incremental. First, route new writes through the module boundary. Then establish a reliable way to synchronize data, backfill historical records, and verify counts and behavior. Only after the new owner is proven should the old write path disappear. A temporary compatibility layer is often safer than a single all-or-nothing cutover.

Build the operational foundation early

Services are not merely smaller applications. They are independently operated applications. Before extracting many of them, establish a repeatable baseline for configuration, logging, health checks, metrics, deployment, and rollback.

Docker can make local environments and deployments more consistent, but a container is not an operational strategy. A PHP service still needs predictable startup behavior, environment-based configuration, structured logs, and a clear distinction between readiness and liveness. Readiness answers whether it can serve traffic now; liveness answers whether it should be restarted.

Also make observability part of the first release. When a request crosses an API gateway, a PHP application, a queue worker, and a new service, logs without correlation identifiers become a scavenger hunt. Propagate a request or trace ID across boundaries, record meaningful errors, and monitor the business outcomes that matter: failed jobs, delayed processing, elevated error rates, and queue backlog.

Keep the system easier to change

The best architecture is not the one with the most services. It is the one that lets a team understand a change, test it, deploy it, and recover from a mistake without heroic coordination. Sometimes that means one modular application. Sometimes it means a few focused services around clear business boundaries.

Decomposition works when it follows real pressure, strengthens ownership, and earns its operational complexity. Start by making boundaries explicit inside the monolith. Extract one capability with a contract you can test and observe. Learn from the result before multiplying services.

A monolith is not something to ditch in a dramatic rewrite. It is a system to evolve with judgment. Done well, the path to scale is not a leap into distributed complexity. It is a sequence of small decisions that make the next change safer than the last.

Blog author portrait

Mihajlo

I’m Mihajlo — a developer driven by curiosity, discipline, and the constant urge to create something meaningful. I share insights, tutorials, and free services to help others simplify their work and grow in the ever-evolving world of software and AI.