Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›System Design›Microservices Architecture

Architecture Patterns

Microservices Architecture

Microservices architecture is an approach where a large application is structured as a collection of small, autonomous services that communicate through well-defined interfaces. This matters because it enables independent scalability, fault isolation, and faster deployment cycles by decoupling the development lifecycle of distinct business domains. You should reach for this architectural style when your system complexity exceeds the cognitive load of a single team or when specific parts of your application require disparate resource demands.

Service Decoupling and Domain Decomposition

The fundamental premise of microservices is the decomposition of a system into bounded contexts. In a monolithic system, code is often tightly coupled through shared memory, leading to a 'big ball of mud' where changes in one domain accidentally break unrelated features. By isolating domains—such as Billing, Inventory, or User Profiles—into separate services, you force developers to think in terms of clear contracts rather than internal implementation details. This separation allows each service to evolve independently. When you design a microservice, you are essentially defining a boundary where the cost of communication over a network is justified by the gain in organizational agility and fault tolerance. Reasoning about this requires understanding that boundaries should align with business capabilities, not technical layers, ensuring that a change in the product requirement only impacts the specific service responsible for that business logic.

// A simple interface definition for a User Service interaction
interface UserProfile {
  id: string;
  email: string;
}

// The service acts as a black box that hides storage implementation
function getUser(userId: string): UserProfile {
  // In a real system, this would involve a network call or internal database lookup
  return { id: userId, email: "user@example.com" };
}

Inter-Service Communication Patterns

Since microservices run in isolation, they must exchange information over a network. The two primary patterns are synchronous Request-Response and asynchronous Event-Driven messaging. Synchronous patterns, like REST, are intuitive but create temporal coupling: if the service you call is down, your request fails. This makes the system fragile. Conversely, asynchronous communication using message queues allows services to operate independently. If an upstream service emits an event, the downstream service can consume it whenever it becomes available, effectively buffering traffic spikes and preventing cascading failures. When choosing between these, consider the latency requirements. Synchronous calls are best for read-heavy operations where immediate results are mandatory, while asynchronous messaging is superior for workflow orchestration and long-running processes where you can trade immediate consistency for increased availability and throughput across the entire distributed ecosystem.

// Simulating a message queue publisher to decouple services
const queue = [];

function publishEvent(eventType: string, payload: any) {
  // Decouples the producer from the consumer via a buffer
  queue.push({ eventType, payload, timestamp: Date.now() });
  console.log(`Event ${eventType} queued for processing.`);
}

Resilience Through Circuit Breakers

In a distributed system, failures are inevitable. A microservice might experience high latency or go offline entirely. If your service blindly keeps trying to call a failing dependency, it risks exhausting its own resources, like thread pools or connection sockets, leading to a total system collapse. A circuit breaker pattern prevents this. It acts as a safety switch: if failure rates exceed a defined threshold, the 'circuit' trips and the caller immediately returns an error or a fallback response without attempting the network request. After a timeout period, the breaker enters a 'half-open' state to test if the service has recovered. This mechanism is crucial for microservices because it limits the blast radius of a failure. By failing fast, you preserve the stability of the caller, allowing the rest of the system to continue functioning while the failing component is addressed or auto-heals.

let state = 'CLOSED'; // 'CLOSED', 'OPEN', 'HALF-OPEN'

function callRemoteService(request) {
  if (state === 'OPEN') return 'Fallback: Service Unavailable';
  
  try {
    // Execute request logic...
    return 'Success';
  } catch (e) {
    state = 'OPEN'; // Trip the breaker
    setTimeout(() => state = 'HALF-OPEN', 5000);
    return 'Fallback: Error handled';
  }
}

Data Management and Consistency

The most challenging aspect of microservices is managing data. In a monolith, you use ACID transactions across all tables. In microservices, every service owns its own database, making global transactions impossible. You must embrace eventual consistency. If a user update needs to propagate to multiple services, you use the Saga pattern: a sequence of local transactions where each step publishes an event to trigger the next. If a step fails, the system executes 'compensating transactions' to undo the changes made by previous steps. This is more complex than a standard database transaction, but it is necessary for maintaining system availability. You must reason about the 'state' of the system as a distributed property rather than a local one, ensuring that every service eventually reaches a consistent state even if they are temporarily out of sync during the process.

// A simplistic saga step executor for distributed consistency
async function executeOrderSaga(order) {
  try {
    await reserveInventory(order);
    await processPayment(order);
  } catch (e) {
    // Compensating transaction if payment fails
    await releaseInventory(order);
  }
}

Observability and Distributed Tracing

When a system is composed of dozens of services, debugging an error is difficult because a single user request might traverse multiple network hops. Traditional logging is insufficient here. You need distributed tracing, where each request is assigned a unique correlation ID that travels across every service boundary. This ID allows you to reconstruct the entire request path in a monitoring tool, showing where time was spent and where failures occurred. Without proper observability, a microservice architecture becomes a black box that is impossible to maintain. You must invest in centralizing logs, metrics, and traces early. Understanding the system's performance requires you to look at the 'golden signals': latency, traffic, errors, and saturation. By instrumenting your code to propagate trace headers, you gain the ability to pinpoint exactly which service in the chain is causing a bottleneck or a critical error.

// Attaching a trace ID to headers to monitor requests across services
function makeAuthenticatedCall(url, traceId) {
  const headers = { 'X-Trace-Id': traceId };
  // Using fetch to pass the trace information downstream
  return fetch(url, { headers });
}

Key points

  • Microservices decompose complex systems into small, independent units to improve maintainability.
  • Effective service boundaries should align with business domains rather than technical implementation details.
  • Synchronous communication causes temporal coupling and should be used cautiously compared to asynchronous patterns.
  • Circuit breakers are essential to prevent cascading failures by isolating failing components from the healthy ones.
  • Data consistency in a microservices environment relies on eventual consistency rather than distributed transactions.
  • The Saga pattern provides a mechanism to manage multi-step distributed transactions with compensation logic.
  • Distributed tracing is a mandatory requirement for debugging requests that span across multiple service boundaries.
  • Observability ensures that developers can monitor the health and performance of the system as a unified whole.

Common mistakes

  • Mistake: Designing microservices based on data entities rather than business domains. Why it's wrong: This leads to highly coupled services that require frequent cross-service coordination. Fix: Use Domain-Driven Design to identify bounded contexts.
  • Mistake: Sharing a single database across multiple microservices. Why it's wrong: This creates a tight coupling at the data layer, negating the benefits of independent deployability and scalability. Fix: Enforce the Database-per-Service pattern.
  • Mistake: Over-relying on synchronous REST calls for inter-service communication. Why it's wrong: It creates cascading failures and reduces overall system availability. Fix: Implement asynchronous messaging patterns using message brokers.
  • Mistake: Neglecting distributed tracing and centralized logging. Why it's wrong: Debugging across multiple isolated services becomes impossible without observability. Fix: Integrate correlation IDs and distributed tracing spans early in development.
  • Mistake: Attempting to decompose a monolithic system into microservices too early. Why it's wrong: This adds significant operational complexity without solving specific scaling or organizational bottlenecks. Fix: Start with a modular monolith and extract services only when team boundaries or scaling needs justify it.

Interview questions

What is the fundamental benefit of a microservices architecture over a monolithic approach?

The fundamental benefit is the ability to achieve independent deployability and scalability. In a monolith, any change to a small feature requires redeploying the entire application, which increases risk and slows down the CI/CD pipeline. With microservices, each service is a self-contained unit that can be developed, tested, and deployed independently. This allows teams to iterate faster, adopt different technology stacks for specific needs, and isolate failures so that a crash in one module does not bring down the entire system.

How do you handle inter-service communication, and what are the trade-offs between synchronous and asynchronous models?

Communication can be synchronous, typically using REST or gRPC, or asynchronous, utilizing message brokers like Kafka or RabbitMQ. Synchronous communication is easier to implement and provides immediate feedback but creates tight coupling and cascading failure risks if one service is down. Asynchronous communication decouples the sender from the receiver, improving system resilience and allowing for better load leveling during traffic spikes. The trade-off is increased complexity in distributed tracing and eventual consistency management, as responses are not immediate.

Why is the API Gateway pattern critical in a microservices environment?

The API Gateway acts as a single entry point for all client requests, abstracting the underlying microservices topology. It is critical because it handles cross-cutting concerns like authentication, rate limiting, logging, and load balancing, which would otherwise be duplicated across every single service. By centralizing these functions, the gateway simplifies client-side interactions and provides a security layer that shields internal service endpoints from the public internet, significantly reducing the attack surface of the overall system.

Compare the 'Database-per-Service' pattern with 'Shared Database' in microservices design.

The 'Database-per-Service' pattern ensures that services are truly decoupled, preventing cross-service schema dependencies and allowing each team to choose the best storage technology for their data access patterns. While it ensures isolation, it complicates data consistency, requiring complex distributed transactions or Saga patterns. Conversely, a 'Shared Database' simplifies data management and transactions but creates a massive coupling bottleneck. If the schema changes, all dependent services break, which violates the core microservices philosophy of autonomy. Therefore, the database-per-service pattern is preferred for large-scale, distributed systems.

How can you implement distributed transactions across microservices, and why is the Saga pattern preferred over Two-Phase Commit (2PC)?

Distributed transactions are difficult in microservices because you cannot use traditional ACID transactions across different databases. Two-Phase Commit is a blocking protocol that is ill-suited for high-concurrency systems because it causes long lock times and becomes a performance bottleneck. The Saga pattern is preferred because it handles distributed transactions as a series of local transactions, each updating its own database. If one step fails, the Saga executes a series of 'compensating transactions' to undo the changes. This approach maintains high availability and throughput by avoiding distributed locks, though it forces developers to design for eventual consistency.

Explain the concept of 'Service Discovery' and how it solves the problem of dynamic infrastructure.

Service Discovery is essential because microservices are frequently deployed, moved, or auto-scaled, meaning their network locations are ephemeral. In a dynamic environment, hardcoding IP addresses is impossible. Service Discovery works by using a registry (like Consul or etcd) where services register their locations upon startup. When a service needs to talk to another, it queries the registry to obtain a healthy instance's address. This mechanism provides client-side or server-side load balancing and ensures that requests are only routed to live instances, preventing service outages due to infrastructure changes.

All System Design interview questions →

Check yourself

1. When is it appropriate to decompose a modular monolith into microservices?

  • A.When the team wants to use multiple programming languages
  • B.When development velocity is hampered by the coupling of independent domain features
  • C.When the application needs to run on a cloud-native platform
  • D.When the application reaches a certain number of lines of code
Show answer

B. When development velocity is hampered by the coupling of independent domain features
Decomposition should solve organizational or scaling bottlenecks. Option 1 is a side effect, not a primary driver. Option 2 addresses the actual business value of decoupling. Option 3 is possible with monoliths. Option 4 is an arbitrary metric that does not indicate a need for architectural change.

2. What is the primary benefit of the Database-per-Service pattern in a microservices architecture?

  • A.It improves read performance by reducing join complexity
  • B.It ensures strict consistency across all system data
  • C.It enforces loose coupling by preventing services from accessing each other's underlying data schemas
  • D.It simplifies the implementation of distributed transactions
Show answer

C. It enforces loose coupling by preventing services from accessing each other's underlying data schemas
Loose coupling is the goal; separate databases prevent services from becoming coupled through shared database schemas. Option 1 is incorrect as joining is often harder. Option 2 is wrong because isolation makes consistency harder. Option 4 is false as distributed transactions become more complex.

3. Why is 'Choreography' often preferred over 'Orchestration' for long-running business processes in complex microservice systems?

  • A.It makes the business logic easier to trace and visualize
  • B.It removes the central point of failure and reduces coupling between the coordinating service and participants
  • C.It guarantees transactional atomicity across all involved services
  • D.It requires less configuration for monitoring and observability
Show answer

B. It removes the central point of failure and reduces coupling between the coordinating service and participants
Choreography uses events to trigger actions, removing a central controller, which lowers coupling. Option 1 is wrong because orchestration is actually easier to visualize. Option 3 is wrong as neither guarantees atomicity. Option 4 is wrong because distributed systems are inherently harder to monitor.

4. A system experiences a 'cascading failure' when one service is overloaded. Which pattern best mitigates this issue?

  • A.The Saga Pattern
  • B.The Circuit Breaker Pattern
  • C.The Database-per-Service Pattern
  • D.The Sidecar Pattern
Show answer

B. The Circuit Breaker Pattern
The Circuit Breaker prevents a failing service from consuming resources across the system by failing fast. The Saga pattern handles failures in distributed transactions, not cascading service load. Database patterns relate to data management, and the Sidecar pattern is for infrastructure cross-cutting concerns.

5. In a distributed microservices environment, how is data consistency typically managed when a business process spans multiple services?

  • A.Using 2-Phase Commit (2PC) protocols across all services
  • B.Accepting eventual consistency through techniques like Sagas and compensating transactions
  • C.Enforcing synchronous ACID compliance at the API gateway layer
  • D.Merging the involved services into a single process to maintain atomic updates
Show answer

B. Accepting eventual consistency through techniques like Sagas and compensating transactions
Distributed systems often rely on eventual consistency via Sagas to avoid the performance penalties of 2PC. Option 1 is outdated and problematic at scale. Option 3 is technically impossible to enforce across remote services. Option 4 defeats the purpose of microservices by creating a monolith.

Take the full System Design quiz →

← PreviousRate Limiting and ThrottlingNext →API Gateway Pattern

System Design

31 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app