Development

Beyond Microservices: Scaling APIs with Evolving System Needs

Beyond Microservices: Scaling APIs with Evolving System Needs

Microservices are often presented as the natural next step once an API starts to grow. More traffic arrives, teams multiply, deployments become slower, and the monolith begins to feel like a constraint. Splitting it up can help—but only when the split reflects real system needs.

The hard truth is that “microservices” is not a scaling strategy by itself. It is a trade: less internal coupling in exchange for network boundaries, distributed data, operational overhead, and more failure modes. A mature backend architecture evolves by making those trades deliberately, not by treating service count as a sign of progress.

Scale the pressure point, not the diagram

An API can struggle for many reasons: expensive database queries, a slow third-party dependency, a long-running export, a noisy tenant, a deployment bottleneck, or an overloaded team. These problems do not all require a new service.

Start by identifying what is actually under pressure. If a product catalog endpoint is slow because it joins large tables and applies several filters, extracting “catalog-service” will not make the query cheaper. It may add an HTTP call to the same expensive query. Better first moves might include an index review, pagination, a narrower response shape, caching, or a read model designed for that access pattern.

Likewise, if PDF generation holds PHP workers open for minutes, the useful boundary may be asynchronous work rather than a separate public service. Put the request on a queue, return a job identifier, and let a worker generate the file outside the request lifecycle. The system has evolved, but it has not yet taken on the cost of a distributed product domain.

Keep the modular monolith longer than feels fashionable

A well-structured monolith is not an architectural failure. It is often the fastest way to preserve transactional consistency, understand a codebase, and ship changes while a domain is still taking shape.

“Modular” is the key word. A PHP application can keep billing, identity, ordering, and notifications in the same deployment while enforcing clear ownership in code. Avoid letting every controller reach into every model. Give each module an application boundary, explicit interfaces, and a limited set of persistence responsibilities.

For example, an order module should ask a pricing module for a price through a defined application service or contract. It should not casually reproduce pricing rules in SQL, a controller, and a background command. That discipline makes future extraction possible, but it also makes the monolith easier to maintain today.

Useful signals that a boundary is becoming real

  • A subsystem has a distinctly different scaling profile, such as image processing or search indexing.
  • A team needs to release and operate a capability on an independent cadence.
  • The domain has stable ownership and language that is understood across the organization.
  • The subsystem can tolerate asynchronous communication or has a clear contract for synchronous calls.
  • Its data can be owned independently without constant cross-boundary joins and transactions.

None of these signals means extraction is mandatory. Together, they make the decision much easier to defend.

Database boundaries are usually the real boundary

It is easy to put an HTTP API in front of a module and call it a microservice. The difficult question is whether it owns its data. If multiple services read and write the same tables directly, the architecture has distributed deployment without distributed ownership.

Shared tables create subtle coupling. A migration intended for one service can break another. A harmless-looking query can depend on an undocumented column. Teams cannot change schemas freely because every database consumer is now part of the release plan.

When extracting a capability, decide which service is authoritative for each piece of data. Other services should receive the information they need through APIs, events, or purpose-built replicated views. That does introduce eventual consistency, so it must be designed into the user experience and business process.

Consider inventory. An order API may submit a reservation request, while inventory remains authoritative over available stock. The order service should not decrement inventory rows on its own. If reservation is asynchronous, the order can enter a pending state until it receives confirmation or failure. This is more explicit than pretending a distributed operation is still one database transaction.

Design for failure before adding service calls

In-process function calls fail predictably. Network calls do not. They can time out, return late, succeed after the caller has given up, or fail while the remote system is still processing the request. Every new synchronous dependency changes the reliability characteristics of the endpoint.

For each service-to-service call, define the practical answers to a few questions:

  • What is the timeout, and is it shorter than the caller’s remaining request budget?
  • Can the operation be retried safely, and how is duplicate work prevented?
  • What does the caller do when the dependency is unavailable?
  • Which response is cached, queued, degraded, or rejected?
  • How will operators correlate one client request across services?

Idempotency matters especially for commands. A payment or order-creation request should carry an idempotency key so that a retry does not create a second charge or duplicate order. Retries without idempotency are not resilience; they are a mechanism for multiplying failures.

Events also need care. Publish them reliably, retain enough context for consumers, and make consumers idempotent. An event handler may run more than once. Treating duplicate delivery as an exceptional impossibility is how data drift begins.

Containers improve packaging, not architecture

Docker is valuable because it standardizes runtime packaging. A PHP application, its extensions, and its process configuration can move through development, testing, and deployment in a repeatable form. That is useful whether the system has one deployable application or twenty.

But containers do not remove the need for observability, configuration management, secrets handling, database migrations, or rollback plans. A fleet of tiny containers with unclear ownership is harder to operate than a carefully packaged monolith.

Keep operational concerns concrete. Health checks should reflect whether an instance can serve its intended traffic. Logs should be structured enough to connect errors to request identifiers. Metrics should distinguish latency, error rate, saturation, and queue depth. Database migrations should be backward-compatible when old and new application versions may run at the same time.

final class CreateOrderHandler
{
    public function __invoke(CreateOrder $command): OrderId
    {
        $existing = $this->orders->findByIdempotencyKey($command->idempotencyKey());

        if ($existing !== null) {
            return $existing->id();
        }

        $order = Order::pending($command);
        $this->orders->save($order);
        $this->outbox->record(new OrderCreated($order->id()));

        return $order->id();
    }
}

The important detail is not the exact class layout. It is the boundary: persist the business change and the event record together, then publish from a reliable background process. This avoids claiming an event was sent when the database transaction later fails.

Let architecture stay reversible

The best evolution path is usually incremental. First improve module boundaries. Then isolate expensive work behind queues. Introduce a dedicated read model where read traffic demands it. Extract one capability when its ownership, data, and operational requirements are clear. Measure the result before repeating the pattern.

Microservices can be an excellent destination for parts of a system. They are a poor default identity for the whole system. The durable goal is simpler: make each change understandable, each dependency intentional, and each scaling decision proportional to the pressure it solves.

That is how APIs grow without becoming a maze of network calls. Not by chasing a fashionable architecture, but by building boundaries that earn the right to exist.

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.