Hasina Razafintsalama

Hasina RAZAFINTSALAMA

← Back to Blog
Architecture

Microservices Architecture: The Principles That Actually Matter

Microservices are not "small services". They are an organizational and architectural bet with real trade-offs. Here are the principles that separate a healthy microservices architecture from a distributed monolith.

2026-07-08·12 min

Migrating to microservices is one conversation. Designing a microservices architecture that does not collapse into a distributed monolith is another. These are the principles that separate the two, whether you are migrating an existing system or building greenfield.

First: do you need them?

Microservices trade simplicity for independent scaling and team autonomy. That trade is worth it once a part of the system genuinely needs to scale or deploy on its own, or once multiple teams block each other in one codebase. Until then, a modular monolith, clear module boundaries and enforced dependencies in a single deployable, gives you most of the structure with none of the network, and it is the right default.

Boundaries follow business capabilities, not technical layers

The most common mistake is slicing services by technical layer: a models service, a controllers service, an auth service that owns nothing. Boundaries should follow business capabilities and bounded contexts. An Orders service, a Shipping service, a Billing service, each owning a complete vertical slice of its domain, from its data to its API. If two services always change together, they are one service.

Each service owns its data, no exceptions

A shared database is the fastest way to a distributed monolith: any schema change now needs coordinated deploys across teams, and the independent services are independent in name only. A service reaches another service's data only through its API or its events, never its tables.

php
// Anti-pattern: Shipping reads the Orders database directly
$order = DB::connection('orders_db')->table('orders')->find($orderId);

// Pattern: Shipping asks the Orders service through its API
$order = Http::get(config('services.orders.url') . "/orders/{$orderId}")->json();

Communication: choose sync or async deliberately

  • Synchronous REST or gRPC only when the caller needs an immediate answer: authentication, a payment authorization.
  • Asynchronous events or queues for anything that can happen eventually: notifications, analytics, audit logs, cross-service updates.
  • Avoid chatty synchronous chains where A waits on B waits on C; latency and failure compound with every hop.

Distributed transactions: sagas and the outbox

You cannot run a database transaction across services. A business operation that spans several services becomes a saga: a sequence of local transactions, each publishing an event that triggers the next, with a compensating action if a step fails. To publish that event reliably in the same transaction as the local write, use the outbox pattern, write the event to an outbox table in the same commit, then a separate process ships it to the broker.

Design for failure, none of it is optional

  • Timeouts and circuit breakers on every network call, so a slow dependency does not cascade into a full outage.
  • Retries with exponential backoff, paired with idempotency keys so a retried write does not duplicate data.
  • Bulkheads: isolate resources per dependency so one struggling downstream cannot exhaust every thread or connection.
  • Graceful degradation: decide what the service does when a dependency is down, before you find out in production.

Observability is a prerequisite, not an add-on

A single user request can touch six services. Without distributed tracing (OpenTelemetry), a correlation ID propagated on every call, and centralized structured logs, debugging that request is guesswork dressed up as an incident report. Track the RED metrics per service, rate, errors, duration, and alert on them.

Independent deployment is the test

The clearest measure of whether you actually have microservices: can you deploy one service, at any time, without coordinating with another team or redeploying anything else. If not, you have a distributed monolith, and the network calls between your services are pure cost.

SignalHealthy microservicesDistributed monolith
DeploymentAny service, any time, independentlyServices must deploy together
DataEach service owns its databaseShared database or cross-service table reads
A downstream outageDegrades one featureCascades across unrelated features
Adding a fieldOne service, one deployCoordinated change across teams
CommunicationMostly async eventsDeep synchronous call chains

The clearest sign of a distributed monolith: you cannot deploy one service without redeploying others, or a bug in one takes down features that should be unrelated. If that is your reality, you have microservices in name and a monolith in practice, now with network latency added.

FAQ

How big should a microservice be?
Size it to a bounded context, one business capability owned end to end, not to a line count. A good test: it can be understood, changed and deployed by one team without coordinating with others. If two services always change together, they should be one.
Does each microservice need its own database?
Yes. A shared database couples services at the schema level, so any change needs coordinated deploys and the independence is only nominal. Services read each other's data through APIs or events, never through the other service's tables.
How do I handle a transaction across microservices?
You cannot use a database transaction across services. Model the operation as a saga: a chain of local transactions, each emitting an event that triggers the next, with compensating actions to undo earlier steps if one fails. Publish those events reliably with the outbox pattern.
What is a distributed monolith?
A system split into separate services that still cannot be deployed or changed independently, usually because they share a database or depend on deep synchronous call chains. It has the operational cost of microservices and the coupling of a monolith, plus network latency.
Microservices or a modular monolith?
Start with a modular monolith: enforced module boundaries in a single deployable. It gives you most of the architectural discipline with none of the distributed-systems cost. Move a module to its own service only when it genuinely needs to scale or deploy independently.

Microservices are a tool for a specific problem: parts of a system that must scale or evolve independently, and teams that must not block each other. Applied there, with data ownership, async communication, failure handling and real observability, they pay off. Applied by default, they are a tax.

Need help with this topic? Microservices Migration

Discover this service