Jump to Category
| 💡 Core Principles & Design | 🗣️ Communication Patterns |
| 💾 Data Management & Consistency | 💪 Resiliency & Fault Tolerance |
| 🚀 Deployment & Observability |
Core Principles & Design
1. Beyond “it’s smaller,” what are the key organizational and technical drivers for adopting a microservices architecture over a monolith?
The primary drivers are organizational scaling and technical velocity.
- Organizational Scaling: It allows for small, autonomous teams to own and operate their services independently. This aligns with Conway’s Law and avoids the communication overhead and bottlenecks of a large, single team working on a monolith.
- Technical Velocity: Teams can develop, deploy, and scale their services independently. A change in one service doesn’t require a coordinated, high-risk deployment of the entire application. This enables faster, more frequent releases.
- Technology Diversity: Different services can use different technology stacks best suited for their specific job, rather than being locked into a single stack.
2. How does Domain-Driven Design (DDD), specifically the concept of a “Bounded Context,” influence how you define microservice boundaries?
A **Bounded Context** from DDD is a conceptual boundary within which a particular domain model is consistent and well-defined. This is the ideal way to define microservice boundaries. Each microservice should own a single Bounded Context.
For example, in an e-commerce system, the concept of a “Product” might mean one thing in the `Catalog` context (with attributes like description, images) and something entirely different in the `Inventory` context (with attributes like stock level, warehouse location). By creating a `CatalogService` and an `InventoryService`, you align your service boundaries with the domain boundaries, leading to services that are highly cohesive and loosely coupled.
Read Martin Fowler’s article on Bounded Context.3. What is the “database per service” pattern and what challenges does it introduce?
This pattern dictates that each microservice must own and manage its own private database. No other service is allowed to access this database directly; they must go through the owner service’s public API. This ensures loose coupling and allows each service to choose the database technology best suited for its needs.
Challenges:
- Distributed Transactions: You can no longer rely on ACID transactions across services. This requires complex patterns like Sagas.
- Data Consistency: Keeping data consistent across multiple services is difficult, often requiring an event-driven approach.
- Joining Data: You cannot perform a simple SQL join across service databases. Queries that need data from multiple services must be done through API composition or by using a denormalized read model (CQRS).
4. What is the Strangler Fig Pattern?
The Strangler Fig Pattern is an architectural approach for incrementally migrating a legacy monolithic application to a microservices architecture. Instead of a risky “big bang” rewrite, you gradually build new microservices around the edges of the old system.
You place a facade or proxy (like an API Gateway) in front of the monolith. As new microservices are built to replace specific pieces of functionality, you update the facade to route traffic for those features to the new services. Over time, the new system “strangles” the old one until the monolith can be safely decommissioned.
Read Martin Fowler’s article on this pattern.5. What are the trade-offs of code sharing between microservices?
Sharing code via libraries can seem efficient but introduces a dangerous form of coupling.
Pros:
- Reduces boilerplate code for common concerns like logging or client generation.
Cons (significant):
- Coupling: A change to the shared library can force a coordinated, simultaneous redeployment of all services that use it, defeating the purpose of independent deployability.
- Versioning Hell: Managing different versions of the shared library across many services can become a major headache.
- Hidden Domain Logic: It’s easy for shared business logic to creep into a library, creating a “distributed monolith.”
Best practice is to share as little code as possible. If code must be shared, it should be for truly generic, cross-cutting concerns, not for business logic.
Communication Patterns
6. Compare synchronous and asynchronous communication between microservices.
- Synchronous (e.g., REST, gRPC): The client sends a request and blocks, waiting for a response from the other service. This is simpler to reason about but introduces tight runtime coupling. If the downstream service is slow or unavailable, the calling service is directly impacted (a “cascading failure”).
- Asynchronous (e.g., message queues, event streams): The client sends a message to a broker and does not wait for a response. The services are temporally decoupled. This improves resilience and scalability, as the calling service is not affected by the availability of the consumer. However, it’s more complex to implement and reason about the end-to-end workflow.
7. What is the difference between Orchestration and Choreography?
- Orchestration: A central component (the “conductor” or orchestrator) explicitly controls the workflow and calls other services in a defined sequence. A service like AWS Step Functions is a classic orchestrator. This approach is easier to visualize and debug.
- Choreography: There is no central controller. Each service emits events, and other services react to those events independently to perform their part of the process. An event bus like Amazon EventBridge is used. This approach is more decoupled, flexible, and resilient, but the overall workflow can be harder to observe.
8. What is a Service Mesh and what problems does it solve?
A **Service Mesh** (like Istio or Linkerd) is a dedicated infrastructure layer for managing service-to-service communication. It works by injecting a “sidecar” proxy alongside each microservice instance. All network traffic between services is routed through these proxies.
It solves common cross-cutting concerns at the platform level, without requiring changes to the application code. These include:
- Observability: Automatic metrics, logs, and distributed traces for all traffic.
- Security: Automatic mutual TLS (mTLS) encryption and fine-grained authorization policies.
- Reliability: Sophisticated traffic management, including retries, timeouts, and circuit breaking.
9. Compare client-side and server-side service discovery.
- Client-Side Discovery: The client service is responsible for querying a service registry (like Consul or Eureka) to get the IP address of a downstream service and then load balancing across the instances itself. This is flexible but pushes complexity into the client library.
- Server-Side Discovery: The client makes a request to a router or load balancer (e.g., a Kubernetes Service or an ALB). The router queries the service registry and forwards the request to an available instance. This is simpler for the client but introduces an extra network hop and requires a highly available router component.
10. When would you choose gRPC over REST for inter-service communication?
You would choose gRPC for high-performance, internal service-to-service communication. Key advantages over REST include:
- Performance: It uses Protocol Buffers (Protobufs), a binary serialization format that is much more compact and efficient than JSON. It also operates over HTTP/2, which supports multiplexing and streaming.
- Strict Contracts: The API contract is strictly defined in a `.proto` file, enabling strong typing and code generation for clients and servers.
- Streaming: It has native support for bidirectional streaming, which is difficult to achieve with REST.
REST is still often preferred for public-facing APIs due to its ubiquity and browser-friendliness.
Data Management & Consistency
11. What is the Saga pattern and how does it handle distributed transactions?
The **Saga pattern** is a failure management pattern for maintaining data consistency across multiple microservices without using traditional distributed locks. A saga is a sequence of local transactions. Each transaction updates data in one service and then publishes an event or triggers the next transaction in the sequence.
If a local transaction fails, the saga executes a series of **compensating transactions** that undo the preceding work. For example, if a `CreateOrder` step succeeds but the `ProcessPayment` step fails, a compensating transaction would be triggered to cancel the order. This ensures the entire operation is either fully completed or fully rolled back.
Explore the Saga pattern on microservices.io.12. What is the Outbox Pattern?
The Outbox Pattern is a solution for reliably publishing events after a database transaction. A common problem is that you might commit a database transaction but then fail to publish the corresponding event to a message broker, leaving the system in an inconsistent state.
The pattern solves this by having the service write both the business data and the event(s) to its own database as part of the same ACID transaction. The event is written to an “outbox” table. A separate, asynchronous process then monitors the outbox table and reliably publishes the events to the message broker. Once published, the event is marked as complete in the outbox table.
Learn about the Transactional Outbox pattern.13. What is CQRS and what problem does it solve?
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates the models used for updating data (Commands) from the models used for reading data (Queries).
- Commands are task-based operations that change state (e.g., `CreateUserCommand`).
- Queries are operations that retrieve data and do not change state.
This separation allows you to optimize the read and write models independently. For example, you can use a normalized, transactional database for writes, but maintain a highly denormalized, optimized read model (perhaps in a different database like Elasticsearch) for fast queries. This is particularly useful in complex domains.
Read Martin Fowler’s article on CQRS.14. What is Event Sourcing?
Event Sourcing is a pattern where you store not just the current state of an entity, but the full, immutable sequence of events that have happened to it. The current state is derived by replaying these events. For example, instead of storing a bank account balance, you would store a log of all `Deposited` and `Withdrawn` events. The balance is calculated by replaying the log.
This provides a complete audit log, allows for powerful temporal queries, and works well with patterns like CQRS.
15. What is eventual consistency?
Eventual consistency is a consistency model which guarantees that, if no new updates are made to a given item, all reads of that item will eventually return the last updated value. In the meantime, reads might return stale data. This is a common trade-off in distributed systems that prioritize availability and performance over strong consistency. Many asynchronous, event-driven microservice architectures are eventually consistent.
Resiliency & Fault Tolerance
16. Explain the Circuit Breaker pattern.
The Circuit Breaker pattern prevents an application from repeatedly trying to call a service that is likely to be failing. It wraps a protected function call in a state machine:
- Closed: Requests are allowed. If failure rates exceed a threshold, the breaker “trips” and moves to Open.
- Open: All subsequent requests are immediately rejected without calling the failing service. This gives the downstream service time to recover.
- Half-Open: After a timeout, it allows a single trial request. If it succeeds, the breaker moves back to Closed; if it fails, it returns to Open.
17. What is the Bulkhead pattern?
The Bulkhead pattern is a way to isolate elements of an application into pools so that if one fails, the others can continue to function. It’s named after the partitioned sections (bulkheads) of a ship’s hull. In a microservices context, this could mean maintaining separate connection pools or thread pools for each downstream service you call. If one service becomes slow or unresponsive, it will only exhaust the resources in its dedicated pool, preventing it from taking down the entire calling application.
18. Why is it important to implement retries with exponential backoff and jitter?
When a call to a downstream service fails due to a transient error, it’s common to retry.
- Exponential Backoff: Instead of retrying immediately, you wait for a progressively longer period between retries (e.g., 1s, 2s, 4s, 8s). This gives the failing service time to recover and prevents a “thundering herd” of retries from overwhelming it.
- Jitter: Adds a small, random amount of time to each backoff delay. This prevents thousands of clients from retrying at the exact same moment after a widespread failure, spreading out the retry attempts and avoiding a coordinated spike in traffic.
19. How do you ensure idempotency in your services?
Idempotency is critical in distributed systems where retries are common. An idempotent operation can be performed multiple times with the same result. For write operations, this is often achieved by having the client generate a unique **idempotency key** (e.g., a UUID) for each logical transaction. The client sends this key with the request. The server then keeps a record of processed idempotency keys. If it receives a request with a key it has already seen, it can safely skip the operation and return the original, stored response.
Deployment & Observability
20. What is a “consumer-driven contract” test?
Consumer-driven contract testing is a pattern for testing the integration between a service provider and a service consumer without running a full end-to-end test. The *consumer* defines a “contract” specifying the exact requests it will make and the responses it expects. This contract is then used to:
- Generate a mock server for the consumer’s tests.
- Run against the real provider service in its CI pipeline to verify that it still honors the contract.
Tools like Pact are used for this. It ensures that a provider doesn’t make a breaking change for its consumers.
21. What are the three pillars of observability?
Observability is the ability to understand the internal state of a system from its external outputs. The three pillars are:
- Logs: Detailed, timestamped records of discrete events. Useful for understanding what happened in a specific request or process.
- Metrics: Aggregated, numerical data over a period of time (e.g., requests per second, error rate, CPU utilization). Useful for dashboards, alerting, and understanding trends.
- Traces: Show the end-to-end journey of a single request as it flows through multiple services. Essential for debugging latency and errors in a distributed system.
22. What is distributed tracing?
Distributed tracing is a method for monitoring requests in a microservices architecture. When a request enters the system, it is assigned a unique `traceId`. As the request propagates from one service to another, this `traceId` is passed along in the request headers. Each service adds its own `spanId` to the trace. This allows a tracing system (like Jaeger or Zipkin) to stitch together the entire end-to-end journey of the request, providing a visual representation of the call graph and helping to pinpoint latency bottlenecks.
23. What is the purpose of health check endpoints?
Health check endpoints are special API endpoints (e.g., `/healthz`) that expose the current health status of a service. They are used by orchestration systems (like Kubernetes) and load balancers to determine if a service instance is running correctly and ready to receive traffic. A good health check should verify not only that the process is running, but also that its critical dependencies (like a database connection) are healthy.
24. Why is centralized configuration management important in a microservices architecture?
In a system with dozens or hundreds of services, managing configuration files for each one is untenable. A centralized configuration service (like Spring Cloud Config, Consul, or AWS Parameter Store) provides a single source of truth for all configuration properties.
This allows you to manage configuration for all services and environments in one place, audit changes, and, in some cases, update configuration dynamically without needing to redeploy services.
25. How do you handle database schema migrations in a microservices environment with zero downtime?
This requires careful, evolutionary changes. A common pattern is:
- Additive Changes First: Add the new column, but make it nullable. Deploy the new version of the service that can read the old schema and write to both the old and new columns.
- Backfill Data: Run a data migration task to populate the new column for all existing records based on the old column’s data.
- Switch Read Path: Deploy a new version of the service that now reads from the new column instead of the old one.
- Remove Old Code/Column: Once you are confident everything is working, deploy a final version of the service that removes the code dealing with the old column. Finally, run a schema migration to drop the old column from the database.