INTERVIEW_QUESTIONS
Design Patterns Interview Questions for Senior Engineers (2026)
Master advanced design patterns interview questions covering creational, structural, and behavioral patterns, SOLID principles application, refactoring strategies, and real-world pattern usage for senior roles.
Why Design Patterns Matter in Senior Engineering Interviews
Design patterns are the shared vocabulary of software engineering. At the senior level, interviewers don't just want you to name patterns — they want to see that you can identify when a pattern is the right solution, when it's over-engineering, and how patterns compose together in real systems. Companies like Google, Amazon, and Stripe test design pattern knowledge because it reveals how you think about code organization, extensibility, and maintainability.
The gap between mid-level and senior engineers often shows up here. A mid-level engineer might implement a Singleton when asked. A senior engineer discusses why Singletons are problematic for testability, offers alternatives (dependency injection), and knows when a Singleton is genuinely the right choice (logger configuration, database connection pools).
This guide covers the most challenging design pattern interview questions with frameworks that demonstrate senior-level thinking. For related topics, see our SOLID principles guide, backend development crash course, and system design interview guide.
1. When would you use the Strategy pattern, and how does it differ from the Template Method pattern?
What the interviewer is really asking: Can you distinguish between composition-based and inheritance-based approaches to achieving the same goal? Do you understand when each is appropriate?
Answer framework:
Both Strategy and Template Method solve the same problem: defining a family of algorithms and making them interchangeable. The difference is in how they achieve it.
Strategy pattern (composition): Define an interface for the algorithm. Implement each algorithm as a separate class. The client holds a reference to the strategy interface and delegates to it. Algorithms can be swapped at runtime.
Example: a payment processing system with PaymentStrategy interface and implementations for CreditCardPayment, PayPalPayment, CryptoPayment. The OrderService holds a PaymentStrategy reference and delegates processPayment() to it.
Template Method (inheritance): Define the skeleton of an algorithm in a base class with abstract methods for the variable parts. Subclasses override the abstract methods to provide specific behavior. The algorithm structure is fixed; only specific steps vary.
Example: a data import pipeline with an abstract DataImporter base class defining readSource() → transform() → loadTarget(). Subclasses like CsvImporter and ApiImporter override readSource() and transform() but share the same pipeline structure.
When to choose Strategy: the algorithm varies independently of the client, you need to swap algorithms at runtime, or you want to test algorithms in isolation. Strategy favors composition over inheritance (generally preferred in modern software design).
When to choose Template Method: the overall algorithm structure is fixed and you only need to vary specific steps, the steps have a defined order, or subclasses need to share common behavior defined in the base class.
A senior insight: in many modern codebases, Strategy with first-class functions replaces both patterns. Instead of defining an interface and implementation classes, pass a function: processPayment(order, paymentFn). This is common in Go, JavaScript/TypeScript, and Kotlin.
See our SOLID principles guide and backend development crash course.
Follow-up questions:
- How does the Strategy pattern relate to the Open/Closed Principle?
- When does the Template Method pattern violate the Liskov Substitution Principle?
- How would you refactor a large switch statement using the Strategy pattern?
2. Explain the Observer pattern and its role in event-driven architectures.
What the interviewer is really asking: Do you understand the publish-subscribe model that underlies much of modern software architecture?
Answer framework:
The Observer pattern establishes a one-to-many dependency between objects. When the subject (publisher) changes state, all registered observers (subscribers) are notified automatically.
Classic implementation: the Subject maintains a list of Observer references. When state changes, it iterates through the list and calls update() on each observer. Observers can register and unregister dynamically.
This pattern is the foundation of:
Event-driven UI frameworks: React's state management (when state changes, all subscribed components re-render), DOM event listeners (addEventListener), and Vue's reactivity system.
Message queuing systems: Kafka topics are subjects, consumer groups are observers. The decoupling enables independent scaling of producers and consumers.
Microservice event systems: Domain events (OrderPlaced, PaymentProcessed) are published to an event bus. Multiple services subscribe independently. This is the backbone of event-driven architectures.
Database triggers and change streams: MongoDB Change Streams and PostgreSQL LISTEN/NOTIFY implement the observer pattern at the database level.
Important considerations:
- Memory leaks: Observers that register but never unregister keep the subject alive (and vice versa in languages with GC). Always provide unsubscribe mechanisms.
- Ordering: In basic implementations, notification order is undefined. If order matters, use a priority mechanism.
- Error handling: If one observer throws an exception, should it prevent other observers from being notified? Typically no — isolate each observer's execution.
- Synchronous vs asynchronous: Synchronous notification blocks the subject until all observers complete. Asynchronous notification (using message queues or event loops) decouples execution timing.
See our how Kafka works, system design interview guide, and distributed systems guide.
Follow-up questions:
- What is the difference between the Observer pattern and the Mediator pattern?
- How does reactive programming (RxJava, RxJS) extend the Observer pattern?
- How would you implement the Observer pattern in a distributed system?
3. When is the Singleton pattern appropriate, and what are the alternatives?
What the interviewer is really asking: Do you understand why Singletons are controversial and can you discuss alternatives? This is a seniority signal.
Answer framework:
The Singleton pattern ensures only one instance of a class exists and provides global access to it. While simple, it has significant downsides that senior engineers must understand.
Problems with Singleton:
- Testing difficulty: Singletons carry state between tests. Mocking a Singleton requires special techniques (service locator pattern, reflection, or framework support). Each test might see state from previous tests.
- Hidden dependencies: Code that calls
Database.getInstance()has a hidden dependency that doesn't appear in its constructor or method signature. This makes the code harder to understand and reason about. - Concurrency issues: Lazy initialization of Singletons in multi-threaded environments requires synchronization (double-checked locking, which is tricky to implement correctly).
- Violates Single Responsibility Principle: The class manages its own lifecycle in addition to its actual responsibility.
Alternatives:
- Dependency injection: Pass dependencies through constructors. A DI framework (Spring, Guice, or manual DI) creates and manages the single instance. The class itself doesn't know or care that it's a singleton.
- Module-level instance: In languages with module systems (Go, Python, TypeScript), create a single instance at the module level and export it. No special pattern needed.
- Service locator: A registry that provides access to shared services. Better than direct Singleton access because the locator can be replaced in tests.
When Singleton is genuinely appropriate: logger configuration (truly global, stateless after initialization), hardware device managers (physical constraint of one device), and application-wide configuration loaded once at startup.
A senior answer: "I rarely use the Singleton pattern directly. Instead, I use dependency injection to manage the lifecycle of shared objects. The DI container ensures only one instance exists (singleton scope) while keeping the class testable and its dependencies explicit."
See our SOLID principles guide and backend development crash course.
Follow-up questions:
- How does dependency injection solve the testing problem with Singletons?
- What is double-checked locking, and why is it error-prone?
- How do Go and Rust handle the Singleton pattern differently from Java?
4. How do you apply the Factory pattern in microservices and plugin architectures?
What the interviewer is really asking: Can you use creational patterns for real architectural problems, not just textbook examples?
Answer framework:
The Factory pattern family (Simple Factory, Factory Method, Abstract Factory) creates objects without specifying their exact class. This enables extensibility and decouples creation from usage.
Factory Method in microservices: A service that processes different message types (OrderEvent, PaymentEvent, ShipmentEvent) uses a factory to create the appropriate handler. The factory maps message types to handler classes. Adding a new message type only requires creating a new handler class and registering it with the factory — no modification of existing code (Open/Closed Principle).
Abstract Factory in plugin architectures: A data pipeline tool supports multiple cloud providers (AWS, GCP, Azure). An Abstract Factory creates provider-specific implementations: CloudFactory.createStorage(), CloudFactory.createQueue(), CloudFactory.createCompute(). The AwsFactory creates S3Storage, SQSQueue, EC2Compute. The GcpFactory creates GCSStorage, PubSubQueue, GCECompute. Application code depends only on the abstract interfaces.
Registry pattern (extension of Factory): Factories can be static (hardcoded mapping) or dynamic (runtime registration). A dynamic registry lets plugins register their handlers at startup: handlerRegistry.register("ORDER_EVENT", OrderEventHandler.class). This is how frameworks like Spring Boot auto-discover and register components.
Practical implementations:
- Java: Spring's
BeanFactoryandApplicationContextare sophisticated factories managed by the DI container. - Go: Constructor functions (
NewOrderService()) serve as simple factories. Interface-based polymorphism replaces class hierarchies. - TypeScript: Factory functions returning objects conforming to interfaces.
Avoid over-engineering: a simple switch statement that creates objects is fine if there are only 3-4 cases and they rarely change. Introduce a Factory when the number of types grows, when creation logic is complex, or when you need runtime extensibility.
See our backend development crash course and system design interview guide.
Follow-up questions:
- What is the difference between Factory Method and Abstract Factory?
- How does the Factory pattern relate to the Dependency Inversion Principle?
- How would you implement a plugin system using factories in Go or TypeScript?
5. Explain the Circuit Breaker pattern and its implementation.
What the interviewer is really asking: Do you know resilience patterns for distributed systems? This bridges design patterns and system design.
Answer framework:
The Circuit Breaker pattern prevents cascading failures in distributed systems by detecting when a downstream service is failing and short-circuiting requests instead of waiting for timeouts.
Three states:
- Closed (normal operation): Requests pass through to the downstream service. The circuit breaker monitors failure rates. If failures exceed a threshold (e.g., 50% of requests in the last 10 seconds), the circuit "opens."
- Open (failing fast): Requests are immediately rejected without calling the downstream service. This prevents overwhelming a struggling service and reduces latency for the caller (fail fast vs wait for timeout). After a configured timeout (e.g., 30 seconds), the circuit enters half-open.
- Half-open (testing recovery): A limited number of requests are allowed through. If they succeed, the circuit closes (service recovered). If they fail, the circuit opens again.
Implementation details:
- Failure counting: Use a sliding window (time-based or count-based) to track recent failures. A rolling window avoids counting old failures.
- Threshold configuration: Set thresholds based on the service's normal error rate. A service with a 1% baseline error rate should have a circuit breaker threshold higher than 1%.
- Fallback behavior: When the circuit is open, return a cached response, default value, or degraded functionality instead of an error. Example: show cached product recommendations when the recommendation service is down.
- Monitoring: Expose circuit breaker state as a metric. Alert when a circuit opens — it indicates a downstream problem that needs investigation.
Libraries: Hystrix (Netflix, now in maintenance mode), Resilience4j (Java, recommended replacement), Polly (.NET), and sony/gobreaker (Go). In Kubernetes, service meshes like Istio implement circuit breaking at the infrastructure level.
The Circuit Breaker is often combined with Retry (retry transient failures) and Bulkhead (isolate resources per dependency) patterns. The order matters: Retry inside Circuit Breaker — if retries consistently fail, the circuit breaker opens to prevent retry storms.
See our distributed systems guide, system design interview guide, and microservices concepts.
Follow-up questions:
- How does the Circuit Breaker pattern interact with retry logic?
- What is the Bulkhead pattern, and how does it complement the Circuit Breaker?
- How would you configure circuit breaker thresholds for a service with variable latency?
6. How do you implement the Repository pattern for data access?
What the interviewer is really asking: Do you separate business logic from data access concerns? Can you design for testability?
Answer framework:
The Repository pattern abstracts data access behind a collection-like interface. Business logic interacts with the repository interface, not directly with the database. This provides a clean separation of concerns and enables testing business logic without a database.
Interface definition:
Implementations:
PostgresOrderRepository: Uses SQL queries with a connection poolMongoOrderRepository: Uses the MongoDB driverInMemoryOrderRepository: Uses a Map for unit testing
Benefits:
- Testability: Unit tests use
InMemoryOrderRepository— no database setup, instant execution - Database migration: Switching from PostgreSQL to MongoDB requires only a new repository implementation, not changing business logic
- Query encapsulation: Complex queries are encapsulated in repository methods, not scattered through business logic
Common anti-patterns:
- Leaky abstraction: Repository methods that expose database-specific concepts (SQL, cursors, connections). The interface should speak the domain language.
- Generic repository:
Repository<T>with only CRUD methods. This ignores the fact that different entities have different query patterns. AUserRepositoryneedsfindByEmail(), which a generic repository doesn't provide. - Over-abstraction: For simple CRUD applications, a repository layer may add unnecessary indirection. Use it when business logic is complex enough to benefit from the separation.
Related patterns: Unit of Work tracks changes to multiple entities and commits them as a single transaction. Combined with Repository, it provides transactional data access: unitOfWork.orders.save(order); unitOfWork.payments.save(payment); unitOfWork.commit();.
See our backend development crash course and SOLID principles guide.
Follow-up questions:
- How do you handle complex queries that span multiple entities in the Repository pattern?
- What is the difference between the Repository pattern and the Active Record pattern?
- How does the Repository pattern work with ORMs like Hibernate or Prisma?
7. Explain the Decorator pattern and how it's used in middleware chains.
What the interviewer is really asking: Do you understand how to compose behavior without modifying existing code? This is one of the most practically useful patterns.
Answer framework:
The Decorator pattern wraps an object with additional behavior while maintaining the same interface. Decorators can be stacked to compose multiple behaviors.
Classic example: a DataSource interface with a read() method. FileDataSource reads raw data. CompressionDecorator wraps any DataSource and adds compression/decompression. EncryptionDecorator wraps any DataSource and adds encryption/decryption. You can compose: new EncryptionDecorator(new CompressionDecorator(new FileDataSource("file.dat"))) — reads are decrypted then decompressed, writes are compressed then encrypted.
Middleware chains are the Decorator pattern applied to request processing:
HTTP middleware (Express.js, Go's http.Handler): Each middleware wraps the next handler, adding cross-cutting concerns. A request passes through: LoggingMiddleware → AuthMiddleware → RateLimitMiddleware → ActualHandler. Each middleware can modify the request, modify the response, short-circuit the chain (return early for unauthorized requests), or measure timing.
gRPC interceptors: Same concept for gRPC — unary and streaming interceptors wrap handlers with logging, tracing, authentication, and error handling.
Go's idiomatic decorator:
Java's InputStream hierarchy is a classic Decorator example: BufferedInputStream(new GZIPInputStream(new FileInputStream("file.gz"))) — each stream wraps the previous one, adding buffering and decompression.
Decorator vs Inheritance: Decorator adds behavior at runtime and can be combined flexibly. Inheritance adds behavior at compile time and creates a rigid class hierarchy. Decorators are preferred when you need to mix-and-match capabilities.
See our backend development crash course and system design interview guide.
Follow-up questions:
- How does the Decorator pattern differ from the Proxy pattern?
- What is the disadvantage of deeply nested decorators?
- How would you implement middleware ordering and dependency management?
8. How does the Event Sourcing pattern work, and when should you use it?
What the interviewer is really asking: Do you understand this advanced architectural pattern that's increasingly common in distributed systems?
Answer framework:
Event Sourcing stores the state of an entity as a sequence of events (facts about what happened) rather than the current state. The current state is derived by replaying all events from the beginning.
Traditional approach: account.balance = 500 (store current state).
Event Sourcing approach: [AccountCreated(0), Deposited(1000), Withdrawn(300), Deposited(200), Withdrawn(400)] → replay gives balance = 500.
Benefits:
- Complete audit trail: Every change is recorded as an immutable event. Perfect for financial systems, healthcare, and compliance-heavy domains.
- Temporal queries: "What was the account balance on March 15?" — replay events up to March 15.
- Event replay: Fix a bug in business logic, replay all events through the corrected logic to recompute state.
- Decoupling: Events can be consumed by multiple downstream systems (read models, analytics, notifications) via event bus.
Challenges:
- Complexity: Significantly more complex than CRUD. Team must understand event-driven thinking.
- Event schema evolution: Events are immutable but their schema must evolve. Use versioned events and upcasters (transforms old event versions to current version).
- Performance: Replaying thousands of events per entity is slow. Snapshots solve this — periodically store the computed state and replay only events after the snapshot.
- Eventual consistency: If using CQRS (separate read and write models), read models are eventually consistent with the event store.
When to use: financial systems (audit requirements), collaborative applications (document editing, multiplayer games), systems requiring undo/redo, and domains where the history of changes is as valuable as the current state.
When NOT to use: simple CRUD applications, systems where current state is all that matters, teams without event-driven experience, or when a simple audit log table would suffice.
Often combined with CQRS (Command Query Responsibility Segregation): writes go to the event store, reads come from optimized read models (projections) that are updated by consuming events.
See our distributed systems guide, how Kafka works, and system design interview guide.
Follow-up questions:
- How do you handle event schema evolution in an event-sourced system?
- What is the Saga pattern, and how does it relate to Event Sourcing?
- How would you implement snapshots for an entity with millions of events?
9. Explain the Adapter and Facade patterns and when you would use each.
What the interviewer is really asking: Can you manage complexity by providing clean interfaces to messy subsystems?
Answer framework:
Adapter pattern: Converts the interface of one class into another interface that the client expects. It allows incompatible interfaces to work together. The adapter wraps the adaptee and translates calls.
Use case: integrating a third-party payment API whose interface doesn't match your application's PaymentGateway interface. The adapter wraps the third-party SDK and translates method calls, parameter types, and error formats to match your interface. When you switch payment providers, you write a new adapter without changing your application code.
Another common use case: legacy system integration. Your new microservice expects JSON over HTTP; the legacy system speaks XML over SOAP. An adapter translates between the two protocols.
Facade pattern: Provides a simplified interface to a complex subsystem. The facade doesn't wrap a single object — it orchestrates multiple subsystem components behind a simple API.
Use case: a video conversion system involves codecs, compression, audio mixing, and metadata handling — each with complex APIs. A VideoConverter facade provides a single method: convert(input, outputFormat). It internally orchestrates the subsystem components.
Another use case: an e-commerce checkout facade that orchestrates inventory checking, payment processing, order creation, notification sending, and analytics tracking behind a single checkout(cart, paymentInfo) method.
Key differences:
- Adapter: makes one interface compatible with another (1:1 wrapping)
- Facade: simplifies a complex subsystem (1:many orchestration)
- Adapter is structural (interface compatibility); Facade is about simplification and hiding complexity
See our backend development crash course and system design interview guide.
Follow-up questions:
- How does the Adapter pattern relate to the Dependency Inversion Principle?
- When does a Facade become a God Object anti-pattern?
- How would you design an adapter layer for a multi-cloud application?
10. How do you apply the Builder pattern for complex object construction?
What the interviewer is really asking: Can you handle construction of objects with many optional parameters cleanly?
Answer framework:
The Builder pattern separates the construction of a complex object from its representation. It's essential when objects have many optional parameters, when the construction process involves multiple steps, or when you want to create immutable objects.
The telescoping constructor anti-pattern: new Order(id, customerId, null, null, "express", null, true, null, items, null) — numerous null parameters, unclear which parameter is which, error-prone.
Builder solution:
Benefits: readable construction, validated construction (the build() method validates required fields and combinations), immutable objects (all fields set during construction, no setters), and fluent API that guides the user.
Step Builder (typed builder): Forces a specific construction order using the type system. Order.builder().withCustomer(cust).withItems(items).withPayment(pay).build() — each step returns a different type that only exposes the next valid step. Prevents invalid intermediate states at compile time.
Real-world examples:
- Java's
StringBuilderfor efficient string construction - OkHttp's
Request.Builderfor HTTP request construction - Protobuf message builders
- Kubernetes client libraries for constructing resource definitions
- Test data builders ("Object Mother" pattern) for creating test fixtures
In languages with named parameters and default values (Kotlin, Python, TypeScript), the Builder pattern is less necessary — the language provides similar functionality natively. Use Builder when the construction logic is complex or when you need validation.
See our backend development crash course and SOLID principles guide.
Follow-up questions:
- How does the Builder pattern differ from the Factory pattern?
- When would you use a Builder vs a constructor with named parameters?
- How do you implement the Builder pattern for immutable objects in Java?
11. Explain the Proxy pattern and its variations (virtual, protection, remote).
What the interviewer is really asking: Do you understand how proxies are used in real systems for lazy loading, access control, and remote communication?
Answer framework:
The Proxy pattern provides a surrogate or placeholder for another object, controlling access to it. The proxy has the same interface as the real object, making it transparent to the client.
Virtual proxy (lazy initialization): Defers creation of an expensive object until it's actually needed. Example: an image viewer where ImageProxy stores the file path and only loads the full image data (potentially megabytes) when display() is called. Before display, the proxy shows a placeholder. This is how ORMs implement lazy loading — related entities are loaded from the database only when accessed.
Protection proxy (access control): Controls access to the real object based on permissions. Example: a DocumentProxy checks the user's role before allowing read(), write(), or delete() operations. If unauthorized, the proxy throws an exception without forwarding to the real document. This pattern is used in Spring Security's method-level security annotations (@PreAuthorize).
Remote proxy: Represents an object in a different address space (another server). The proxy handles serialization, network communication, and deserialization. gRPC stubs, RMI stubs, and REST client wrappers are all remote proxies. The client calls methods on the proxy as if the object were local; the proxy handles the network details.
Caching proxy: Wraps an object and caches results of expensive operations. If the same request comes in, returns the cached result. This is a combination of Proxy and Decorator patterns.
Logging proxy: Wraps an object and logs all method calls (parameters, return values, exceptions, timing). AOP (Aspect-Oriented Programming) frameworks like AspectJ use dynamic proxies for cross-cutting concerns.
Java dynamic proxies (java.lang.reflect.Proxy) and Go's interface-based design make it easy to create proxies at runtime. Python's __getattr__ enables transparent proxying.
See our backend development crash course and distributed systems guide.
Follow-up questions:
- How does the Proxy pattern differ from the Decorator pattern?
- How are dynamic proxies implemented in Java?
- How does gRPC use the remote proxy pattern?
12. How do you use the Command pattern for undo/redo and task queuing?
What the interviewer is really asking: Can you apply behavioral patterns to real system features?
Answer framework:
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
Each command object contains: the action to execute, all necessary parameters, the receiver (the object that performs the actual work), and optionally, how to undo the action.
Undo/Redo implementation: Maintain a stack of executed commands. Each command implements execute() and undo(). Executing a command pushes it onto the undo stack. Undoing pops the last command and calls its undo() method, pushing it onto the redo stack. This is how text editors, graphic design tools, and IDEs implement undo.
Task queuing: Commands are serialized and placed in a queue (Kafka, SQS, database table). Workers dequeue and execute commands. This decouples request submission from execution and enables: retry (re-execute the command), scheduling (execute at a future time), and audit (log all commands).
CQRS (Command Query Responsibility Segregation): Commands (writes) and queries (reads) are handled by separate models. Commands modify state through command handlers. Queries read from optimized read models. This separates write-optimized and read-optimized data structures.
Real-world examples:
- Database transaction logs (each SQL statement is a command with undo via ROLLBACK)
- GUI frameworks (menu items, toolbar buttons, keyboard shortcuts all trigger command objects)
- Job queues (Sidekiq, Celery — each job is a serialized command)
- Event sourcing (events are essentially commands that have been validated and applied)
See our how Kafka works, distributed systems guide, and system design interview guide.
Follow-up questions:
- How does the Command pattern relate to Event Sourcing?
- How do you handle commands that can't be undone (e.g., sending an email)?
- How would you implement command batching for performance optimization?
13. When and how do you refactor code to introduce design patterns?
What the interviewer is really asking: Do you apply patterns pragmatically based on emerging needs, or do you force patterns prematurely?
Answer framework:
Design patterns should emerge from refactoring, not be applied speculatively. The principle: write simple code first, refactor to patterns when complexity demands it.
Refactoring signals that indicate a pattern is needed:
Growing switch/if-else chains → Strategy or Factory pattern. When a new type requires modifying multiple switch statements, extract each case into a strategy class.
Duplicated code with slight variations → Template Method or Strategy. Extract the common structure and parameterize the varying parts.
Complex object construction → Builder pattern. When constructors have more than 4-5 parameters or many optional parameters.
Tight coupling to specific implementations → Dependency Injection + Interface extraction. When you can't test a class because it directly instantiates its dependencies.
Cascading failures in service calls → Circuit Breaker pattern. When a failing downstream service causes your service to hang.
Inconsistent state management → State Machine pattern. When an object's behavior depends on its state and the state transitions are getting complex.
Refactoring approach:
- Identify the code smell (duplication, long methods, large classes, switch statements)
- Determine which pattern addresses the smell
- Refactor incrementally — don't rewrite entire modules at once
- Write tests before refactoring to ensure behavior is preserved
- Verify the pattern reduces complexity rather than adding it
Anti-pattern: "design pattern disease" — applying patterns everywhere because you can, not because you should. A class with a single implementation doesn't need an interface and a factory. Apply patterns when the complexity justifies the indirection.
See our SOLID principles guide and career path to staff engineer.
Follow-up questions:
- How do you decide whether to refactor to a pattern or keep the simple implementation?
- What is Martin Fowler's approach to refactoring and patterns?
- How do you measure whether a refactoring improved code quality?
14. Explain the Saga pattern for managing distributed transactions.
What the interviewer is really asking: Do you understand how to maintain consistency across microservices without distributed transactions?
Answer framework:
The Saga pattern manages data consistency across microservices by breaking a distributed transaction into a sequence of local transactions. Each service executes its local transaction and publishes an event. If any step fails, compensating transactions are executed to undo the previous steps.
Two coordination approaches:
Choreography (event-driven): Each service listens for events and decides what to do next. Order Service creates order → publishes OrderCreated → Payment Service processes payment → publishes PaymentProcessed → Inventory Service reserves stock → publishes StockReserved → Shipping Service schedules delivery.
If Payment Service fails, it publishes PaymentFailed. Order Service listens for this event and cancels the order (compensating transaction).
Pros: decentralized, no single point of failure, services are independent. Cons: harder to understand the overall flow, difficult to debug, risk of cyclic dependencies.
Orchestration (centralized coordinator): A Saga Orchestrator defines the sequence of steps and manages the flow. The orchestrator sends commands to each service and handles responses. If a step fails, the orchestrator triggers compensating transactions in reverse order.
Pros: clear flow definition, easier to debug and monitor, centralized error handling. Cons: single point of failure (the orchestrator), additional service to maintain.
Compensating transactions must be idempotent (safe to retry if the compensation message is delivered twice). They should undo the business effect, not necessarily the technical operation. Example: compensation for "reserve inventory" is "release inventory," not "delete the reservation record."
Real-world example: an e-commerce order saga — Create Order → Reserve Inventory → Process Payment → Schedule Shipping. If Payment fails: Release Inventory → Cancel Order. Each step has a corresponding compensation.
See our distributed systems guide, how Kafka works, and microservices concepts.
Follow-up questions:
- How do you handle a compensation that fails?
- When would you choose choreography over orchestration?
- How does the Saga pattern compare to two-phase commit?
15. How do you apply the Strangler Fig pattern for legacy system migration?
What the interviewer is really asking: Can you apply patterns at the architectural level, not just the code level? This is a senior/staff-level concern.
Answer framework:
The Strangler Fig pattern (named after strangler fig trees that gradually envelop and replace their host tree) incrementally replaces a legacy system by routing functionality to a new system piece by piece, until the legacy system can be decommissioned.
Implementation:
-
Facade/proxy layer: Place a routing layer (API gateway, reverse proxy, or facade service) in front of the legacy system. Initially, all traffic goes to the legacy system.
-
Incremental migration: Identify a bounded context or feature to migrate. Build the new implementation. Update the routing layer to send traffic for that feature to the new system while everything else continues going to the legacy system.
-
Validation: Run the new implementation in shadow mode — process requests with both old and new systems, compare results, and fix discrepancies.
-
Cutover: Switch traffic for the migrated feature to the new system. Monitor for issues.
-
Repeat: Migrate the next feature. Over time, more and more traffic goes to the new system.
-
Decommission: When all features are migrated, shut down the legacy system.
Key benefits: reduces risk (each migration is small and reversible), provides continuous delivery (the system is always functional), and allows incremental learning (the team builds expertise in the new technology as they migrate).
Routing strategies:
- URL-based routing: Requests to
/api/v2/ordersgo to the new system;/api/v1/customersgoes to the legacy system. - Feature flag-based: A percentage of traffic is routed to the new system for gradual rollout.
- User-based: Specific user groups (internal users, beta testers) are routed to the new system first.
Challenges: shared database (both systems may need to read/write the same data — use CDC to sync), cross-system transactions (use eventual consistency and Saga pattern), and maintaining two systems during migration (operational overhead).
See our system design interview guide, backend development crash course, and career path to staff engineer.
Follow-up questions:
- How do you handle shared data between the legacy and new systems during migration?
- What is the Branch by Abstraction pattern, and how does it relate to Strangler Fig?
- How do you prioritize which features to migrate first?
Common Mistakes in Design Pattern Interviews
-
Naming patterns without understanding trade-offs — Listing patterns is not enough. Discuss when a pattern helps, when it hurts, and what alternatives exist.
-
Applying patterns prematurely — Over-engineering simple code with unnecessary abstractions is a red flag. Senior engineers apply patterns when complexity demands it.
-
Ignoring language idioms — Patterns differ across languages. Go doesn't use inheritance, so the Template Method pattern looks different. TypeScript has first-class functions, making Strategy a simple function parameter.
-
Not connecting patterns to system design — Patterns like Circuit Breaker, Saga, and Event Sourcing bridge code-level design and system architecture. Discuss them in context.
-
Forgetting about testing — Every pattern choice affects testability. Discuss how patterns enable or hinder testing.
-
Treating the GoF book as the definitive list — Modern patterns (Circuit Breaker, Saga, Strangler Fig, CQRS) are equally important for senior engineers.
How to Prepare
Week 1: Review the most commonly used patterns (Strategy, Factory, Observer, Decorator, Adapter, Builder). Implement each in your primary language.
Week 2: Study architectural patterns (Circuit Breaker, Saga, Event Sourcing, CQRS, Strangler Fig). Understand when each is appropriate.
Week 3: Practice refactoring exercises. Take a codebase with code smells and refactor using appropriate patterns.
Week 4: Study how patterns are used in open-source projects you use (Spring, Express, React, Kubernetes client libraries).
For comprehensive preparation, see our system design interview guide and explore the learning paths. Check our pricing plans for full access.
Related Resources
GO DEEPER
Master this topic in our 12-week cohort
Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.