Back to Developer
System Design & Architecture
Design principles, patterns, and architecture — SOLID, DDD, CQRS, clean architecture, and microservices.
What is a microservice and what are its advantages and challenges compared to a monolithic architecture?
A microservice is a small, independently deployable application responsible for a specific business capability. Advantages: decoupling allows independent development, deployment, and scaling; fault isolation prevents a single failure from bringing down the entire system. Challenges: increased operational complexity (inter-service communication, distributed tracing, versioning), network overhead, and harder integration testing and debugging.
What is REST and what best practices do you follow when designing a REST API?
REST (Representational State Transfer) is an architectural style that uses HTTP for client-server communication. Each resource is identified by a URL and manipulated via standard HTTP methods (GET, POST, PUT, DELETE). Best practices include: stateless communication, clear resource naming, proper use of HTTP status codes (200, 404, 500), separation of concerns in layers, and documenting with OpenAPI/Swagger.
How would you ensure a microservice is scalable and fault-tolerant?
Key strategies: 1) Error handling with retries, backoff, and circuit breakers (e.g., Resilience4j). 2) Horizontal scalability by designing stateless services behind a load balancer. 3) Observability via structured logging, metrics (Micrometer/Prometheus), and distributed tracing (Zipkin/OpenTelemetry). 4) Testing with unit and integration tests plus documentation (Swagger/OpenAPI). 5) Container-based deployment with Docker and Kubernetes for auto-scaling.
What is the difference between GET, POST, PUT, and DELETE in HTTP? Which are idempotent?
GET retrieves a resource without side effects — idempotent. POST creates a new resource or submits data — not idempotent (two identical POSTs may create two resources). PUT replaces or updates an entire resource — idempotent (same request produces same result). DELETE removes a resource — idempotent (deleting something already gone doesn't change state). Idempotency is important for safe retries in distributed systems.
How would you deploy microservices in the cloud using AWS?
For cloud deployment on AWS: use ECS/EKS or Elastic Beanstalk for container orchestration, Lambda for serverless functions, S3/EBS for storage, RDS for relational databases and DynamoDB for NoSQL, ElastiCache (Redis/Memcached) for caching, CloudFront as CDN, CloudWatch/CloudTrail for monitoring, SQS/SNS for async messaging, and IAM for security. CI/CD pipelines with AWS CodePipeline, GitHub Actions, or Jenkins ensure automated, reproducible deployments across dev, staging, and production environments.
What is the Saga pattern in microservices?
The Saga pattern coordinates a series of local transactions across multiple microservices to maintain data consistency without relying on a distributed transaction (2PC). Each service performs its local transaction and publishes an event; if a step fails, compensating transactions are triggered to undo previous steps. This approach preserves service autonomy while handling cross-service business processes reliably.
How would you design a REST API endpoint that accepts a character matrix and a word list, then returns the matched words?
Define a POST endpoint (e.g. POST /api/wordfinder) that accepts a JSON body with two fields: the matrix (array of strings) and the word list (array of strings). The response returns the top matched words as a JSON array. POST is preferred over GET because the payload can be large and query-string encoding of a matrix is impractical.
How do you return the top-N most frequently found words from a word-search result?
Store the count of matrix occurrences for each matched word in a dictionary/map, then sort the entries by count in descending order and take the first N. If N is fixed (e.g. 10), a partial sort or a min-heap of size N is more efficient than a full sort for large result sets.
What performance considerations apply when searching a 64×64 character matrix for a large word list?
A brute-force scan is O(R × C × W × |words|). Prebuilding a Trie from the word list reduces word-matching to a single matrix traversal — each character is checked against the Trie node, giving O(R × C × W) total. For the given 64×64 constraint, even brute force is acceptable, but the Trie approach scales better as the word list grows.
How would you design a notification dispatch system so that adding a new channel (e.g., WhatsApp) requires no changes to existing code?
Define a common interface (e.g.,
NotificationChannel) with a send(notification) method, then implement it separately for each channel (Email, SMS, Push). A dispatcher selects the correct implementation at runtime based on the notification's channel field. This follows the Open-Closed Principle: the system is open for extension (new channel class) but closed for modification (no changes to the dispatcher or existing channels).How do you ensure that a user can only read or modify their own notifications?
Extract the authenticated user's ID from the JWT claims inside the auth middleware, then scope every database query with a
WHERE user_id = :currentUserId condition. Never rely on a user-supplied userId in the request body or path for ownership checks, as it can be manipulated by the client.Why is it important to trigger the notification send immediately on creation rather than as a separate manual step?
Triggering the send on creation keeps the operation atomic from the user's perspective: a notification that exists has always been dispatched. It also simplifies the API surface (no separate /send endpoint) and reduces the risk of orphaned notifications that were created but never sent. If the dispatch can fail, it should be handled via retries or a background job queue rather than requiring client-side orchestration.
Why is an in-memory cache (e.g., Caffeine) insufficient for a multi-replica deployment, and what is the recommended alternative?
Each replica holds its own independent cache, so a value cached in replica A is invisible to replica B, leading to inconsistent behavior and redundant external calls. The recommended alternative is a distributed cache such as Redis, where all replicas read and write to a shared store. Spring Boot integrates with Redis via spring-boot-starter-data-redis and can be configured as the CacheManager backend transparently.
How do you structure a docker-compose.yml to run a Spring Boot application alongside a PostgreSQL database?
Define two services: one for the Spring Boot app (built from a Dockerfile or pulled from Docker Hub) and one using the official postgres image. Use an environment block to pass DB credentials and a depends_on clause so the app waits for the database service. Expose the app port (e.g., 8080) and mount a named volume for PostgreSQL data persistence. A healthcheck on the postgres service ensures readiness before the app starts.
How should asynchronous audit logging be designed so that failures in logging never affect the main API response?
The main request handler persists its result and then publishes an event or calls a @Async method to write the audit record. The async handler wraps all persistence logic in a try-catch and silently discards failures (or sends them to a dead-letter queue for later inspection). Because the main thread never awaits the async result, any logging failure is fully isolated from the API consumer.
How does Webpack Module Federation work with Next.js SSR and what is the main pitfall?
Standard Module Federation is client-side only—remote modules are loaded at runtime in the browser. When Next.js tries to server-render a page importing a remote component, it fails because the remote bundle is unavailable on the server. The fix is to use @module-federation/nextjs-mf (node-federation) or to wrap remote components in a dynamic import with ssr: false. The key risk is a hydration mismatch: if the server renders a fallback but the client loads the real remote component, React throws a hydration error.
What strategy would you use to migrate a legacy monolithic frontend to microfrontends?
Use the Strangler Fig Pattern—avoid a full rewrite. First set up a new shell application (e.g., Next.js). Then route legacy traffic via rewrites or a reverse proxy so existing URLs keep working. Next, pick one low-risk vertical (e.g., Settings) and rebuild it as a microfrontend exposed via Module Federation. Finally, migrate routes one by one until the legacy app can be decommissioned. This keeps the old system live until each piece is replaced and verified.
What is the N+1 problem in GraphQL and how do you solve it?
The N+1 problem occurs when resolving a list of N items triggers N additional queries—one per item. For example, fetching 10 users plus each user's address fires 1 + 10 = 11 queries instead of 1. The standard solution is DataLoader, which batches all sub-requests into a single query within one event-loop tick. For caching, since all GraphQL operations hit a single POST endpoint CDN caching is limited; use client-side normalised caching (Apollo Client) or Persisted Queries so CDNs can cache hashed queries as GET requests.
What is the difference between a relational database and a NoSQL database? When would you use each?
Relational databases (SQL) store data in tables with rows/columns, enforce relationships via primary/foreign keys, use SQL for complex queries, and guarantee ACID transactions. NoSQL databases store data as documents, key-value pairs, columns, or graphs; they offer flexible schemas and are optimized for horizontal scalability and fast read/write. Use relational when you need strong consistency and clear relationships (e.g., banking). Use NoSQL when you need speed, flexibility, and high scalability (e.g., logs, product catalogs, sessions).
What is a database transaction and what does ACID stand for?
A transaction is a set of operations executed as a single unit of work — if any operation fails, the entire transaction is rolled back to maintain data integrity. ACID stands for: Atomicity (all-or-nothing execution), Consistency (database transitions from one valid state to another), Isolation (concurrent transactions don't interfere with each other), and Durability (once committed, data persists even if the system crashes).
What does the Open-Closed Principle state, and how does it apply to a multi-channel notification system?
The Open-Closed Principle (OCP) states that a software entity should be open for extension but closed for modification. In a notification system, it means each new channel is delivered as a new class implementing a shared interface, while the core dispatch logic never needs to change. Violating OCP would result in a growing switch/case in the dispatcher every time a channel is added.
How would you model the relational database schema for a notification system with user ownership?
Two core tables suffice:
users (id, email, password_hash, created_at) and notifications (id, user_id FK → users.id, title, content, channel, created_at, updated_at). The channel column can be a string enum. An index on notifications.user_id speeds up the common query of fetching all notifications for a given user.What do you know about unit testing in Java? How would you design a test for a service component?
Unit tests verify the behavior of small code units (methods/classes) in isolation. In Java, JUnit provides the test structure and Mockito creates mocks/stubs to simulate external dependencies like repositories. To test a service, you mock the repository, define expected behavior, invoke the service method, and assert results. Tests should cover positive cases, negative cases, and edge cases to ensure correctness and enable safe refactoring.
What HTTP status codes should a word-finder API return for different error scenarios?
Return 200 OK with results on success, 400 Bad Request when the matrix exceeds the 64×64 limit or the input is malformed, and 500 Internal Server Error for unexpected backend failures. Clear error messages in the response body help the frontend display meaningful feedback to the user.
How does token-based authentication work in a RESTful API?
On login the server validates the credentials and returns a signed JWT. The client stores this token and attaches it to subsequent requests via the
Authorization: Bearer <token> header. The server verifies the signature and extracts the user's identity from the token claims without maintaining server-side session state.What HTTP methods and URL patterns would you use for a RESTful notifications CRUD API?
Standard REST conventions:
POST /notifications (create), GET /notifications (list own), GET /notifications/:id (read one), PUT /notifications/:id or PATCH /notifications/:id (update), DELETE /notifications/:id (delete). All endpoints should be protected by an authentication middleware that validates the JWT before the route handler executes.What is the correct way to store user passwords in a database?
Never store plain-text passwords. Use a slow, salted hashing algorithm such as bcrypt, Argon2, or scrypt. These algorithms are deliberately computationally expensive, making brute-force or dictionary attacks impractical even if the database is compromised. The salt is stored alongside the hash so each password hash is unique even for identical passwords.
What HTTP status code should a REST API return when a rate limit is exceeded, and what should the response contain?
The API should return 429 Too Many Requests. The response body should include a human-readable message explaining the limit (e.g., 'Maximum 3 requests per minute allowed'). Optionally, a Retry-After header can indicate when the client may retry. This follows RFC 6585 and helps clients implement proper back-off strategies.
What is the purpose of a refresh token?
A refresh token allows an application to obtain a new access token when the current one expires, without requiring the user to log in again. Access tokens are kept short-lived (minutes) to limit damage if stolen; the longer-lived refresh token is stored more securely (e.g., in an HttpOnly cookie) and is exchanged server-side for a fresh access token.
What are the four symptoms of software design degradation described in the guide, and what does each mean?
The four symptoms are: **Rigidity** (the software becomes hard to change even for simple tasks, with estimates growing ever larger); **Fragility** (changes cause breakage in multiple unrelated parts of the codebase); **Immobility** (reusing code from other projects or other parts of the same project is practically impossible due to heavy dependency baggage); and **Viscosity** (it is easier to do things the wrong way than the correct way, and the development environment itself is slow and inefficient).
What is the Single Responsibility Principle (SRP) and how does it relate to cohesion and coupling?
SRP states that a software module should have one and only one reason to change, where that reason is its responsibility. It is closely related to cohesion and coupling: we want to increase cohesion among things that change for the same reasons and decrease coupling between things that change for different reasons. When a class has more than one responsibility, changes to one concern can inadvertently affect another, making the code harder to read, test, and maintain.
What does the Open/Closed Principle (OCP) mean, and how is it typically implemented?
OCP states that software modules should be open for extension but closed for modification. 'Open for extension' means new behaviour can be added as requirements change; 'closed for modification' means adding that new behaviour should not require altering the existing source code of the module. In practice, OCP is implemented through polymorphism, using interfaces or abstract classes so that new behaviour is introduced by adding new code rather than changing old code.
What does the Liskov Substitution Principle (LSP) require, and why does it caution against blindly mapping the real world to an OO model?
LSP requires that objects in a program should be replaceable by instances of their subtypes without altering the correct functioning of the program. In practice, any subclass must honour the behaviour contract of its parent class. LSP cautions against blindly mapping the real world to an OO model because there is no one-to-one equivalence between both models; what seems like a valid 'is-a' relationship in the real world may violate the behavioural contract in code.
What is the Dependency Inversion Principle (DIP) and what are its two key rules?
DIP states that software entities should depend on abstractions, not on concrete implementations. Its two key rules are: (1) high-level modules should not depend on low-level modules — both should depend on abstractions; and (2) abstractions should be defined based on the needs of the consumer/client, not on the capabilities of the implementation, otherwise the abstraction will be tightly coupled to the implementation and lose flexibility. This enables replacing components without affecting consumers and makes testing easier via mock objects.
What is the DRY (Don't Repeat Yourself) principle and why does it apply to logic rather than just code?
DRY states that every piece of functionality should have a single, unambiguous, authoritative representation within a system. Crucially, DRY applies to logic (the logical function), not merely to code syntax: three methods with different code but the same logical purpose (e.g., all open a database connection) violate DRY. When DRY is applied effectively, a change to any part of the process requires a change in only one place, reducing the risk of inconsistencies, decreasing code size, and saving time through reuse.
What is Inversion of Control (IoC), and which design patterns are implementations of this principle?
IoC is a principle in object-oriented design where control over different types of program flow (including object creation and dependency wiring) is delegated to a third party, achieving low coupling. It increases modularity and produces classes that are testable, maintainable, and extensible. Design patterns that implement IoC include: Service Locator, Dependency Injection, Template Method, Strategy, Abstract Factory, and Observer. The principle is also known as the 'Hollywood Principle' — 'Don't call us, we'll call you.'
What is the Law of Demeter (LoD) and what kind of code does it aim to prevent?
The Law of Demeter (also known as the Principle of Least Knowledge or 'Don't talk to strangers') states that a method of an object should only interact with: (1) methods of the object itself, (2) its arguments, (3) any object created within the method, and (4) direct properties/fields of the object. It aims to prevent deep call chains like
object.getX().getY().getZ().doSomething(), which create strong coupling to the internal structure of the involved classes. Applying LoD reduces coupling, improves reusability, and makes code easier to test.What is the 'Composition over Inheritance' principle and when is it preferred?
This principle states that classes should achieve polymorphic behaviour and code reuse through composition (containing instances of other classes that implement the desired functionality) rather than through inheritance, whenever possible. With inheritance we structure classes around what they *are*; with composition we structure them around what they *do*. Composition is preferred because inheritance locks in rigid, tightly coupled hierarchies early in a project, making future changes difficult. Composition should be used when the HAS-A relationship holds, while inheritance is appropriate only when IS-A genuinely holds and the hierarchy is simple.
What are Kent Beck's four rules of simple design, and in what order of importance are they listed?
Kent Beck's four rules, ordered by relevance, are: (1) **Tests pass** — every feature should work as expected and be verified by tests; (2) **Expresses intent** — the code is self-explanatory, easy to understand, and communicates its purpose; (3) **No duplication (DRY)** — logical duplication should be minimised to avoid fragility; (4) **Minimum number of elements** — the number of components, classes, and methods should be reduced to the essential, eliminating unnecessary complexity. Note: there is debate about whether rules 2 and 3 should share equal priority, and rule 4 is often seen as a consequence of applying rules 2 and 3 continuously.
What is the Boy Scout Rule in software development and what mindset does it promote?
The Boy Scout Rule, drawn from the scouts' motto of leaving a campsite cleaner than they found it, states that whenever a developer sees code that can be improved, they should improve it regardless of who wrote it. The goal is to prevent code degradation over time by making small, safe, incremental improvements that help the next developer. It promotes a team-over-individual mindset: the overall quality of the project matters more than individual task completion. Applying this rule requires a solid understanding of SOLID principles.
What is the Last Responsible Moment principle and why does it recommend deferring design decisions?
The Last Responsible Moment principle recommends deferring design decisions — especially irreversible ones — until the last possible moment: the point at which NOT making a decision would cost more than making it. The rationale is that the longer a decision is kept open, the more information accumulates to make the right choice. In software development it is common to start building features before requirements are fully defined, so premature, hard-to-reverse decisions based on incomplete information are a significant risk.
What is the 'Encapsulate What Varies' principle and which well-known design patterns are based on it?
'Encapsulate What Varies' states that when parts of an application are identified as likely to change, they should be isolated and encapsulated in abstractions so that changes do not affect other parts. It is supported by SRP and OCP. The benefits are twofold: variations in requirements affect only the encapsulated module (reducing fragility and increasing reusability), and new requirements are met by adding new elements rather than modifying existing ones (reducing rigidity). Many design patterns are based on this principle, including Abstract Factory, Factory Method, Adapter, Bridge, Decorator, Iterator, Observer, State, Strategy, Template Method, and Visitor.
What is a Value Object in the context of Domain-Driven Design, and how does it differ from an Entity?
A Value Object is an immutable type identified solely by the values of its properties; two Value Objects are equal if all their properties match. An Entity, by contrast, has its own unique identity (an identifier), so two Entity instances are considered different even if all their properties are identical.
What is the Shared Kernel pattern in Domain-Driven Design, and what are its key constraints?
The Shared Kernel is a subset of the domain model (along with its associated code and database design) that two teams agree to share in order to reduce duplication and simplify integration. This shared subset is special: it cannot be changed freely and must not be modified without consulting the other team. When changes are made, all tests from both teams must pass before the change is accepted.
What is the Customer/Supplier pattern in DDD, and what organizational challenges can arise from it?
Customer/Supplier describes a relationship between two bounded contexts where the downstream component (customer) consumes the output of the upstream component (supplier), with all dependencies flowing in one direction. Challenges arise when the supplier team fears breaking the customer's system, limiting its freedom to evolve, or when the customer is helpless against supplier-driven changes. These problems are best resolved by formalizing the relationship through a documented API, a change calendar, and joint planning.
What is the Anticorruption Layer (ACL) pattern, and when should it be used?
The Anticorruption Layer is an isolation layer placed between a new system and a legacy or poorly designed external system. It translates requests in both directions between the two domain models without requiring significant modification of the external system. It is used to ensure that the design of the application is not constrained by dependencies on external subsystems, and it was first described by Eric Evans in his book 'Domain-Driven Design'.
How is an Anticorruption Layer typically organized internally?
An ACL is typically composed of three complementary elements: a Facade, which provides a simplified and specialized interface to the external system without changing its model; an Adapter, which wraps the facade and translates calls into semantically equivalent requests the external system understands; and a Translator, a lightweight stateless object responsible for converting conceptual objects or data between the two models. Together these elements handle the full translation between bounded contexts.
What is CQRS (Command-Query Responsibility Segregation) and what problem does it solve?
CQRS is an architectural pattern that separates read operations (Queries) from write operations (Commands) into two independent models. The write side handles state changes and may include business validation logic, while the read side returns data without modifying state and can optimize its data representation for display. This separation allows each side to be scaled, secured, and evolved independently, although it adds additional complexity to the system.
In what scenarios is CQRS most beneficial?
CQRS is most beneficial when many users access the same data and each must perform multi-step processing; when there is a clear asymmetry between read and write operation volumes, allowing independent scaling; and when you want to let the UI and business rules evolve independently. It is generally not recommended for simple systems because it requires double the maintenance effort for both models.
What is the Dependency Rule in Clean Architecture, and why is it important?
The Dependency Rule states that an inner layer must be completely isolated from outer layers; inner layers cannot depend on outer layers, but outer layers may know about inner layers. This rule ensures that as a project grows, new code can be added without breaking existing inner logic, and that the core business rules are independent of frameworks, databases, and other infrastructure details.
What are the three main layers in a DDD-based Clean Architecture and what is the responsibility of each?
The Domain layer is the core of the application, containing entities, value objects, aggregates, and domain services that encode the main business logic. The Application layer coordinates domain objects to fulfill user requests; it contains no business logic itself but orchestrates use cases using application services. The Infrastructure layer provides technical capabilities (e.g., database persistence, messaging) to upper layers and must be completely decoupled from the domain layer so that changing a persistence engine does not impact the rest of the system.
What is Hexagonal Architecture (Ports and Adapters), and what is the role of a Port versus an Adapter?
Hexagonal Architecture, introduced by Alistair Cockburn, organizes an application so that all inputs and outputs pass through well-defined connection points that isolate the business logic from external tools. A Port is an interface that specifies how an external tool can use the business logic or how the business logic uses that tool. An Adapter is a class that implements or wraps a port, transforming one interface into another so that an external tool (e.g., a web server, a database) can communicate with the application core.
What is the difference between Driving Adapters and Driven Adapters in Hexagonal Architecture?
Driving Adapters (Primary Adapters) initiate actions in the application—for example, a web controller that receives an HTTP request and invokes an application use case. Driven Adapters (Secondary Adapters) react to instructions from the business logic and connect it to backend tools such as databases or message queues—for example, a repository implementation that persists data to MySQL. Inversion of Control is used throughout: the business logic depends only on port interfaces, never on concrete adapter implementations.
How does CQRS work with a Command Bus, and how does this differ from using CQRS without a Bus?
Without a Bus, the controller has a direct dependency on a Command or Query object, which itself contains and executes the use-case logic. With a Bus, the controller depends on the Bus and dispatches a Command or Query (which acts only as a data carrier); the Bus routes the message to the appropriate Command Handler, which contains the actual use-case logic. Using a Bus decouples the request from its execution, improving extensibility and enabling cross-cutting concerns such as logging or transactions to be handled by the Bus.
What testing strategy does Clean Architecture recommend, and why?
Clean Architecture recommends that the majority of tests be unit tests, because they cover the widest range of business logic paths without requiring any framework, database, or infrastructure tool. Integration tests are also needed to verify that infrastructure implementations work correctly together, but they do not need to re-test all business logic paths. End-to-end tests (acceptance, UI, API) are the most reliable but also the most expensive and fragile, so they should be kept to a reasonable proportion.
What is Event-Driven Architecture (EDA) and what are its three key components?
Event-Driven Architecture is a pattern that promotes asynchronous communication between independent components using events, and is common in microservices applications. Its three key components are: Event Publishers, which emit events when a state change occurs; a Message Broker (router), which filters and routes events to interested consumers; and Event Consumers, which subscribe to specific event types and process them. Publishers and consumers are fully decoupled, allowing each to be scaled, updated, and deployed independently.
What are the main benefits of Event-Driven Architecture?
Key benefits include: independent scaling and error handling, since services only interact with the message broker and remain unaware of each other; agile development, because the broker handles filtering and routing automatically without custom polling code; reduced costs through a push-based model that eliminates continuous polling; easier implementation of flow-control patterns such as backpressure; and no penalty for slow consumers, since each consumer processes events independently without blocking faster consumers.
How should inter-component communication be handled in a Clean/Hexagonal architecture to maintain decoupling?
When one component needs functionality that belongs to another component, a direct method call would create tight coupling. Instead, communication should be mediated by a mechanism such as an Event Dispatcher that routes events between components, or at minimum through a well-defined public API exposed by the target component. This keeps components independent and allows each bounded context to evolve without breaking others.
What is Event Sourcing and how does it differ from traditional state storage?
In Event Sourcing, instead of storing the current state of an entity, the application stores an ordered sequence of state-change domain events. The current state is reconstructed by replaying those events. This makes saving state always atomic (one event = one operation) and provides a reliable audit trail plus the ability to query entity state at any point in time. A key limitation is that querying by arbitrary fields requires building separate materialized views.
In an event-driven microservices architecture, what consistency guarantee do cross-service transactions provide, and how does that differ from ACID?
Cross-service transactions implemented via a message broker offer eventual consistency (BASE guarantees), not ACID. Each service atomically updates its own database and publishes an event, but the overall system may be temporarily inconsistent until all downstream services have processed those events.
What are the main drawbacks of an event-driven architecture in microservices?
The programming model is more complex and requires specific learning. Applications must implement compensation mechanisms to recover from application-level failures, handle temporarily inconsistent data, and subscribers must detect and ignore duplicate events.
What is the atomic update problem in event-driven microservices, and why is it critical?
When a service must both update its database and publish an event, these two operations must happen atomically. If the service crashes after updating the database but before publishing the event, the system becomes inconsistent. The standard solution is a distributed transaction involving both the database and the message broker, but this forces a trade-off between availability and consistency.
How do microservices coordinate a multi-step business transaction using the Saga pattern with events?
In the Saga pattern, each step of the transaction is handled by a separate microservice that updates its local entity and publishes an event to a message broker. That event triggers the next microservice in the sequence. The message broker guarantees at-least-once delivery, enabling the overall transaction to span multiple services without a distributed ACID transaction, relying instead on eventual consistency.
What are the three original categories of design patterns, and why can overusing them be harmful?
The three original categories are Creational, Structural, and Behavioral. Over time, new categories like Concurrency patterns emerged. Overusing or applying patterns unnecessarily can lead to over-engineering, resulting in an overly complex system with inefficient design, poor performance, and maintenance problems.
What is the Builder design pattern and what are its main participants?
The Builder pattern separates the construction logic of an object from its representation. Its main participants are: Builder (abstract interface for creating products), ConcreteBuilder (concrete implementation that creates a specific type of product), and Director (responsible for using the Builder to construct objects).
What is the Singleton pattern and when is it typically used?
The Singleton pattern ensures that only one instance of a class exists, providing a single global access point to it. That instance is responsible for initialization, creation, and access to class properties. It is commonly used when controlling access to a single physical resource (e.g., an exclusive-use file) or when data must be available to all objects in the application (e.g., a logger instance). Care must be taken with exclusive-access concurrency issues.
What is Dependency Injection and how does it relate to Inversion of Control?
Dependency Injection (DI) is a design pattern that removes the responsibility of creating instances from a component and delegates it to another. An object receives its dependencies (services) from an external injector instead of creating them itself. The client only needs to know the interfaces of the services, not their concrete implementations. DI is a way to achieve Inversion of Control (IoC).
What is the Service Locator pattern and how does it differ from Dependency Injection?
The Service Locator pattern uses a central registry (the ServiceLocator) that, on demand, returns the component needed for a given task. The key difference from Dependency Injection is that Service Locator requires an explicit request to obtain the dependency, whereas in DI the dependency is provided automatically. Critics argue it makes software harder to test, while proponents say it simplifies component-based applications. Like DI, it is another implementation of the IoC principle.
What is the Abstract Factory pattern and what are its main participants?
Abstract Factory provides an interface for creating families of related objects without specifying concrete classes. The client uses the generic factory interface and does not know which concrete objects it receives. Its participants include: Client, AbstractFactory (defines factory interfaces), ConcreteFactory (creates a family of concrete products), AbstractProduct (interface for a generic product family), and ConcreteProduct (specific product implementations). It is also an implementation of the IoC principle.
What is the Decorator pattern and what advantages does it offer over inheritance?
The Decorator pattern dynamically adds responsibilities to an object, providing a flexible alternative to subclassing for extending functionality. Its participants are Component (interface), ConcreteComponent, Decorator (holds a reference to a Component and delegates to it), and ConcreteDecorator. Key advantages include: it is more flexible than inheritance, it allows adding and removing responsibilities at runtime, it avoids deep class hierarchies and multiple inheritance, and it limits component responsibilities.
What is the Observer pattern and when should it be applied?
The Observer pattern defines a dependency between objects so that when one object (Subject) changes its state, all dependent objects (Observers) are notified and can react. The Subject maintains a list of Observers and provides methods to subscribe or unsubscribe. It should be applied when a change in one object requires changing others and the exact number is unknown, or when an object should notify others without knowing who they are. It respects the Open/Closed Principle since new Observers can be added without modifying the Subject.
What is the Command pattern and what capabilities does it enable?
The Command pattern encapsulates a request as an object, allowing a common interface to invoke diverse actions. A Client creates a Command object (typically passing the Receiver), and the Invoker stores and triggers execution by calling the Command's execute method. Encapsulating the request as an object enables additional capabilities such as queuing, logging, and undo/redo operations, because the action request is decoupled from its execution.
What is the Strategy pattern and how does it relate to the Open/Closed Principle?
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently across clients. Behaviors should not be inherited but encapsulated using interfaces, so that new algorithms can be added without modifying existing context or strategy interfaces. This directly supports the Open/Closed Principle (OCP): classes are open for extension but closed for modification.
What is the State pattern and how does it differ from using conditional statements?
The State pattern allows an object to alter its behaviour when its internal state changes, making it appear to change class. Each state is represented by a separate class implementing a common interface, replacing complex conditional statements. It respects the Open/Closed Principle and the Single Responsibility Principle. State transitions are atomic for the Context, preventing inconsistent internal states.
What is the Template Method pattern and how does it demonstrate Inversion of Control?
The Template Method pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses, which can redefine certain steps without changing the overall algorithm structure. At runtime, the algorithm executes by sending the template message to an instance of a concrete subclass, which fills in the deferred details through inheritance. It demonstrates Inversion of Control because the high-level code does not decide which algorithm to run; instead, a lower-level algorithm is selected at runtime.
What is the Front Controller pattern in JEE and what are its main components?
The Front Controller pattern uses a single class as an intermediary between the client and requested resources, centralising common operations such as authentication and error handling to avoid code duplication. Its main components are: FrontController (intercepts all web requests and delegates to the Dispatcher), Dispatcher (coordinates actions to resolve requests with help from a Helper), Helper (contains business logic), and View (displays the result to the client).
What is an antipattern and what must it include to be recognised as one?
An antipattern is a commonly applied but bad solution to a recurring problem. To be recognised as one it must: describe a bad solution, analyse the causes that led to it, list the symptoms and consequences that identify it, and finally present a refactored solution showing how to move from the bad design to a well-designed one.
What is the Blob antipattern and how should it be refactored?
The Blob antipattern occurs when one all-powerful class monopolises all procedures and business logic while other classes contain only data, resulting from incremental development without proper responsibility distribution. Refactoring involves moving portions of logic to other classes, creating smaller specialised objects, or introducing a coordinator class, and preventing it requires planning the application architecture before writing code.
What is the Lava Flow antipattern and how do you resolve it?
Lava Flow describes code that has grown organically without a defined architecture, typically originating as a prototype that reached production, leaving undocumented and often unused code that no one dares to remove. Resolution requires stopping new feature development, analysing the entire system to identify what is actually used, redefining the architecture from current business requirements, refactoring, and documenting thoroughly.
What is the Poltergeists antipattern and how is it fixed?
Poltergeists are classes with very limited responsibilities and short lifecycles that exist only to trigger actions in other classes, adding unnecessary abstractions and redundant navigation paths to the design. They are fixed by removing them entirely and moving their initialisation or triggering logic into the classes they previously invoked.
What is the Spaghetti Code antipattern and what are its main causes?
Spaghetti Code is characterised by an overly complex and incomprehensible control-flow structure with minimal relationships between objects, methods oriented to processes rather than objects, and no use of inheritance or polymorphism. Common causes include developer inexperience with object-oriented technologies, ineffective or absent code reviews, and a lack of analysis and design before implementation.
What is the Golden Hammer antipattern and how can it be prevented?
Golden Hammer is the tendency to use the same familiar technology, framework, or language to solve every problem regardless of whether it is the best fit, often driven by comfort, large prior investment, or organisational inertia. Prevention requires fostering a culture of continuous learning, staying current with new technologies, and hiring people with diverse technical backgrounds.
What is the Data Access Object (DAO) pattern and what problem does it solve?
The DAO pattern abstracts and encapsulates all access to a data source, managing the connection to obtain and store data. It decouples the business layer from the data source implementation, so the application does not depend on a specific database engine. Its main value today lies in testability: by mocking the DAO interface, business classes can be tested without real database connections. The concrete DAO implementation uses low-level APIs for data operations.
How does the Outbox pattern solve the atomic update problem in event-driven systems?
The Outbox pattern introduces an EVENT table in the service's own database that acts as a message queue. Within a single local transaction, the service updates its business entities and inserts an event record into the EVENT table. A separate thread or process then polls the EVENT table, publishes those events to the message broker, and marks them as published using another local transaction, ensuring atomicity without a distributed transaction.
What is the transaction log mining approach to publishing events, and what are its trade-offs?
Transaction log mining involves a dedicated thread or process that reads the database's transaction log and publishes corresponding events to the message broker whenever data changes. A key benefit is that it guarantees one event per update and cleanly separates event publishing from business logic. The main drawback is that the transaction log format is database-specific and can change between versions, and it can be difficult to construct high-level business events from low-level log entries.
Parts of this section are adapted from a software-design study guide (Izertis, 2024), licensed under CC BY-SA 4.0. This derived content is shared under the same license.