Skip to main content

FAQ

When should I choose microservices over a monolith?

Choose microservices when you have a concrete, present need for independent deployment and scaling of distinct parts of the system, teams large enough to own services autonomously, and the operational maturity to run a distributed system. The dominant guidance is to start with a well-structured monolith, ideally a modular monolith with clear internal boundaries, and extract services only when a real pressure forces it. Splitting too early converts simple in-process calls into network calls with their own failure modes, latency, and eventual consistency, and it imposes the full operational tax of observability, CI/CD, and on-call across many services before you have the volume to justify it.

How do I decide where to draw service boundaries?

Draw boundaries along business capabilities and Domain-Driven Design bounded contexts rather than along technical layers. A good boundary owns a cohesive slice of the domain, owns its own data, and changes for one set of business reasons, so that most changes touch a single service. Watch for chatty synchronous call chains and frequent shared-data needs across a boundary, which are signals that the seam is in the wrong place. It is usually cheaper to keep modules together in one service until the right boundary becomes clear than to split early and have to merge or re-cut services later.

Should services communicate synchronously or asynchronously?

Use synchronous request-response (REST or gRPC) when the caller genuinely needs an immediate answer to proceed, and keep such chains short to avoid coupling availability and latency. Prefer asynchronous messaging or events for cross-service workflows that can tolerate eventual consistency, because it decouples services in time, absorbs load spikes, and keeps one service's outage from blocking another. A common healthy pattern is synchronous reads at the edge with asynchronous, event-driven propagation of state changes behind the scenes. Whatever you choose, design for failure: timeouts and retries for synchronous calls, idempotent consumers and dead letter queues for asynchronous ones.

How can I run a transaction that spans multiple services?

You generally cannot and should not use a distributed ACID transaction across services, because two-phase commit blocks on failure and scales poorly, and because each service owns its own database. Instead use the saga pattern: model the business transaction as a sequence of local transactions, each in one service, and define a compensating action to undo each completed step if a later step fails. Sagas can be choreographed through events or orchestrated by a central coordinator, and they trade strict atomicity for eventual consistency plus explicit compensation. Make every step idempotent so retries are safe.

What is the dual-write problem and how do I avoid it?

The dual-write problem occurs when a service must update its database and publish an event for the same change, and a crash between the two leaves them inconsistent: the data is saved but no event is sent, or vice versa. You cannot make a database write and a broker publish atomic across two systems. The standard solution is the transactional outbox: write the event into an outbox table in the same local transaction as the business change, then have a separate relay (often using change data capture) read the outbox and publish to the broker. This guarantees the event is published if and only if the state change committed.

How do I keep one failing service from taking down the whole system?

Engineer for fault isolation. Put a timeout on every remote call so a slow dependency cannot hang your threads, and use a circuit breaker to fail fast once a dependency is clearly unhealthy, giving it room to recover. Isolate resources with the bulkhead pattern so one saturated dependency cannot exhaust all your thread or connection pools. Add bounded retries with exponential backoff and jitter for transient errors only, and provide graceful fallbacks or degraded responses where possible. Together these patterns contain failures locally instead of letting them cascade.

How should I handle data that several services need?

Respect the database-per-service rule: do not let other services read your database directly. To share data, expose it through your API for synchronous needs, or publish domain events that interested services consume to build their own local read models or caches. This duplication is intentional and is the price of loose coupling and independent deployability. Keep the duplicated copies eventually consistent through events, and treat the owning service as the single source of truth.

Do I need Kubernetes to run microservices?

No. Microservices are an architectural style, not a specific platform, and you can run them on plain virtual machines, a managed container service, or a platform-as-a-service. Kubernetes is popular because it automates scheduling, scaling, self-healing, rolling updates, and service discovery, which become valuable as the number of services grows. For a small number of services, Kubernetes can be more operational complexity than it is worth, and a simpler platform may serve better until you genuinely need the orchestration.

What is an API gateway and do I always need one?

An API gateway is a single entry point in front of your services that handles cross-cutting concerns such as routing, authentication, rate limiting, TLS termination, and sometimes response aggregation. It simplifies clients and keeps each service focused on business logic, but it is also a component you must make highly available and must avoid turning into a smart, bloated bottleneck. You do not always need one, but it is common once you have several externally exposed services, and a Backend-for-Frontend per client experience is a useful variant when client needs differ significantly.

How do I do testing in a microservices architecture?

Follow the testing pyramid adapted for distribution: many fast unit tests, a healthy layer of integration tests that exercise a service against real infrastructure (using tools like Testcontainers), and a small number of end-to-end tests. Because full end-to-end tests across many services are slow and brittle, lean heavily on consumer-driven contract testing (for example with Pact) to verify that providers still satisfy what their consumers expect, catching breaking API changes in CI without a full environment. Test failure paths explicitly, including timeouts, retries, and broker delivery, since those are where distributed systems break.

How do I debug a request that spans many services?

Use distributed tracing. Generate a correlation or trace ID when a request enters the system and propagate it through every downstream call and log line, so you can reconstruct the full path and timing of one logical request. Instrument with OpenTelemetry and export traces to a backend such as Jaeger, and ensure your structured logs include the trace and span IDs. Combined with metrics and aggregated logs, this gives you the observability needed to find which hop introduced the latency or the error.

How do I version a microservice API without breaking consumers?

Evolve APIs in a backward-compatible way within a major version: add new optional fields and new endpoints rather than removing or renaming existing ones or tightening validation. When a truly breaking change is unavoidable, introduce a new major version and run both in parallel long enough for consumers to migrate, then deprecate and remove the old one on a published schedule. Use consumer-driven contracts to verify in CI that you have not broken any known consumer. Tolerant reading on the consumer side, where consumers ignore fields they do not understand, also reduces accidental breakage.

What is the difference between orchestration and choreography?

Both coordinate a multi-service workflow but in opposite ways. Orchestration uses a central coordinator that explicitly calls each step and decides what happens next, which makes the whole flow visible and easy to change in one place but couples the steps to the orchestrator. Choreography has each service react to events and emit its own, with no central brain, which maximises decoupling and autonomy but makes the end-to-end flow harder to see and reason about. Many systems mix both: choreography for loosely coupled reactions and orchestration for complex sagas that need clear control.

How do I secure communication between services?

Treat the internal network as untrusted and adopt zero-trust principles: authenticate and encrypt service-to-service traffic, commonly with mutual TLS so each side verifies the other's certificate. Authorise calls based on the calling service's or end user's identity, propagating identity via signed tokens such as JWTs rather than trusting network position. A service mesh can provide mTLS, identity, and policy transparently without changing application code. Keep secrets out of code and images, store them in a secret manager, and rotate them.

What are common microservices anti-patterns to avoid?

The most damaging is premature decomposition: splitting into many services before boundaries or operational maturity exist. Closely related is the distributed monolith, where services are nominally separate but so tightly coupled (shared database, synchronous lock-step deploys, chatty chains) that you get distributed-systems pain without the autonomy benefits. Other frequent mistakes are a shared database across services, putting business logic in a smart middleware bus, ignoring failure handling so faults cascade, and building services so fine-grained that operational overhead dwarfs their value. The cure is to start coarse, draw boundaries around business capabilities, and split only under real pressure.

How small should a microservice be?

There is no line-count or single-capability rule that works universally; the useful heuristic is that a service should be independently deployable, own its data, encapsulate a cohesive business capability, and be ownable by one team. If a service is so small that it cannot do anything useful without many synchronous calls to its neighbours, it is too small and you have created chattiness and coupling. Prefer boundaries that minimise cross-service calls and let each service evolve on its own cadence over boundaries that merely maximise the number of services.