How Common Design Patterns Are Applied in Backend Architecture: A Deep Dive into architect-awesome
The architect-awesome repository catalogs 23 classic design patterns alongside SOLID principles, demonstrating how Singleton, Chain-of-Responsibility, Strategy, and other patterns solve specific backend challenges like request pipelining, service decoupling, and cross-cutting concerns.
The open-source knowledge base maintained by xingshaocheng/architect-awesome serves as a comprehensive reference for backend architects, mapping each pattern to concrete implementation scenarios found in enterprise Java and microservices codebases. By grounding abstract pattern definitions in real-world backend contexts—such as servlet filters for Chain-of-Responsibility or runtime metrics collectors for Singleton—the repository illustrates how common design patterns applied in backend architecture yield modular, testable, and scalable systems.
SOLID Foundations: The Six Principles Powering Backend Patterns
Before diving into the 23 concrete patterns listed in the repository’s Design Patterns section, architect-awesome establishes the six SOLID principles as non-negotiable foundations. These principles ensure that pattern implementations do not introduce rigidity or fragility into backend services.
| Principle | Backend Implementation Impact |
|---|---|
| Single Responsibility Principle (SRP) | Each service or module handles one business concern. For example, a UserService manages only user data, while OrderService handles order logic, preventing bloated "god classes" in service layers. |
| Open/Closed Principle (OCP) | New features are added by extending existing abstractions rather than modifying tested code. Payment processors, for instance, extend a base PaymentHandler to support new gateways without altering core transaction logic. |
| Liskov Substitution Principle (LSP) | Substitutable implementations keep APIs stable. A Cache interface accepts both RedisCache and MemoryCache implementations without requiring changes to calling services. |
| Interface Segregation Principle (ISP) | Small, focused interfaces prevent "fat" contracts. Separating ReadRepository from WriteRepository ensures that read-only consumers are not burdened with write-method dependencies. |
| Dependency Inversion Principle (DI) | High-level modules depend on abstractions, not concrete implementations. Inversion of Control (IoC) containers wire concrete classes at runtime, enabling testability through mock injection and configuration flexibility. |
These fundamentals are referenced throughout the pattern descriptions in README.md, ensuring that every pattern—from Singleton to Observer—is applied in a manner that respects SOLID boundaries.
Core Design Patterns for Backend Systems
The repository enumerates 23 classic patterns, but several appear repeatedly in backend architectures due to their specific utility in handling HTTP requests, managing shared resources, and decoupling business logic.
Singleton: Global Service Coordination
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. In backend systems, this is critical for resources such as configuration managers, thread pools, and metrics collectors where duplicate instances would cause state inconsistency or resource exhaustion.
According to the Singleton section in README.md, a classic Java example is java.lang.Runtime#getRuntime(). In microservices, a MetricsCollector singleton aggregates telemetry without race conditions:
public final class MetricsCollector {
private static volatile MetricsCollector INSTANCE;
private MetricsCollector() { /* init resources */ }
public static MetricsCollector getInstance() {
if (INSTANCE == null) {
synchronized (MetricsCollector.class) {
if (INSTANCE == null) {
INSTANCE = new MetricsCollector();
}
}
}
return INSTANCE;
}
public void record(String name, long value) { /* … */ }
}
Chain-of-Responsibility: Request Processing Pipelines
The Chain-of-Responsibility pattern decouples senders from receivers by giving multiple objects a chance to handle a request. In backend architecture, this manifests as HTTP filter chains, middleware stacks, and validation pipelines.
The repository’s Chain-of-Responsibility section cites javax.servlet.Filter#doFilter() as the canonical implementation. Each filter (authentication, logging, compression) processes the request and optionally passes it to the next handler:
public interface Filter {
void doFilter(HttpRequest req, HttpResponse res, FilterChain chain);
}
public class FilterChain {
private final List<Filter> filters = new ArrayList<>();
private int index = 0;
public void add(Filter filter) { filters.add(filter); }
public void doFilter(HttpRequest req, HttpResponse res) {
if (index < filters.size()) {
filters.get(index++).doFilter(req, res, this);
}
}
}
// Example filter implementation
public class AuthFilter implements Filter {
public void doFilter(HttpRequest req, HttpResponse res, FilterChain chain) {
if (!req.isAuthenticated()) {
res.setStatus(401);
return;
}
chain.doFilter(req, res);
}
}
Strategy: Pluggable Algorithms
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Backend services use this to support varying business rules, such as pricing calculations, cache eviction policies, or payment processing methods.
In the repository context, Strategy enables runtime selection of algorithms without conditional spaghetti code. A cache system might switch between LRU and FIFO strategies via dependency injection:
public interface EvictionStrategy {
void evict(Cache cache);
}
public class LruStrategy implements EvictionStrategy {
public void evict(Cache cache) { /* remove least-recently-used */ }
}
public class Cache {
private final EvictionStrategy strategy;
public Cache(EvictionStrategy strategy) {
this.strategy = strategy;
}
public void put(String key, Object value) { /* … */ }
public void clean() { strategy.evict(this); }
}
Factory: Object Creation Encapsulation
The Factory pattern (and Abstract Factory) delegates object creation to specialized classes, decoupling client code from concrete implementations. This is essential in data access layers where the underlying database might vary between environments (MySQL in production, H2 in testing).
The repository references factories under discussions of "对象创建的封装". A typical backend implementation abstracts DAO creation:
public interface UserDao { User findById(Long id); }
public class MySqlUserDao implements UserDao { /* … */ }
public class PgUserDao implements UserDao { /* … */ }
public class DaoFactory {
public static UserDao createUserDao(String db) {
return switch (db) {
case "mysql" -> new MySqlUserDao();
case "postgres" -> new PgUserDao();
default -> throw new IllegalArgumentException("Unsupported DB");
};
}
}
Observer: Event-Driven Decoupling
The Observer pattern establishes a subscription mechanism to notify multiple objects about events. In backend architecture, this enables event-driven microservices, domain event publishing, and loose coupling between business logic and side effects (email sending, audit logging).
The repository lists Observer as critical for decoupling producers from consumers. A lightweight event bus implementation demonstrates the pattern:
public interface EventListener<E> { void onEvent(E event); }
public class EventBus {
private final Map<Class<?>, List<EventListener<?>>> listeners = new ConcurrentHashMap<>();
public <E> void register(Class<E> type, EventListener<E> listener) {
listeners.computeIfAbsent(type, k -> new CopyOnWriteArrayList<>()).add(listener);
}
@SuppressWarnings("unchecked")
public <E> void publish(E event) {
List<EventListener<?>> list = listeners.getOrDefault(event.getClass(), List.of());
for (EventListener<?> l : list) {
((EventListener<E>) l).onEvent(event);
}
}
}
Additional Patterns in Backend Contexts
While the repository enumerates 23 classic patterns, several others merit mention for their specific backend utility:
- Inversion of Control (IoC) / Dependency Injection (DI): Referenced in the IoC section, this pattern enables containers like Spring to wire dependencies at runtime, decoupling configuration from usage.
- Aspect-Oriented Programming (AOP): Listed under AOP, this pattern extracts cross-cutting concerns (logging, transactions, security) from business logic.
- Proxy: Used for remote service invocation (gRPC, RMI) or lazy loading, adding caching or access control without modifying target classes.
- Decorator: Dynamically adds responsibilities to objects, such as wrapping a
DataSourcewith aCachingDataSource. - Builder: Constructs complex objects step-by-step, essential for immutable DTOs and HTTP request builders.
- Adapter: Integrates third-party libraries (payment SDKs, external APIs) with internal domain models by translating interfaces.
Layered Application: Mapping Patterns to Backend Tiers
The architect-awesome repository implicitly organizes patterns by architectural layer, demonstrating how common design patterns applied in backend architecture solve specific tier-level concerns.
Presentation Layer
At the HTTP boundary, patterns manage request routing and response composition:
- MVC (Model-View-Controller) separates routing logic from business rules and view rendering, forming the backbone of Spring MVC and similar frameworks.
- Facade provides simplified interfaces over complex subsystems, hiding intricate order-processing steps behind a clean
OrderFacade. - Decorator wraps request/response objects to add compression, encryption, or logging transparently.
Service and Domain Layer
Business logic remains flexible and testable through behavioral patterns:
- Strategy encapsulates varying algorithms (pricing models, discount rules) that can be swapped at runtime via dependency injection.
- Template Method defines algorithm skeletons in abstract classes, allowing subclasses to refine specific steps without altering the overall workflow—common in batch processing frameworks.
- Command encapsulates requests as objects, enabling transactional rollbacks, task queues, and undo functionality.
- State manages complex object lifecycle transitions (order workflows, payment states) without proliferating conditional statements.
Data Access Layer
Persistence abstraction relies on creational and structural patterns:
- Factory and Abstract Factory create DAO families for different database vendors (MySQL, PostgreSQL) without exposing concrete classes to business logic.
- Repository and Iterator provide uniform collection-like access to domain objects while hiding query complexity.
- Flyweight minimizes memory footprint for large datasets by sharing immutable value objects across multiple contexts.
Integration and Infrastructure Layer
External communication and cross-cutting concerns utilize:
- Proxy for remote service invocation (gRPC stubs, RMI) or lazy loading with added caching layers.
- Adapter to bridge third-party SDKs (payment gateways, external APIs) with internal domain models.
- Observer for event-driven architectures, decoupling message producers from consumers via event buses or message queues.
- Mediator to centralize complex component communication, often realized through message brokers.
Cross-Cutting Concerns
System-wide functionality integrates via:
- AOP (Aspect-Oriented Programming) to modularize logging, transaction management, and security without scattering code through business classes.
- Singleton for managed access to shared resources like configuration managers and thread pools.
- Builder for constructing complex configuration objects or immutable DTOs in a readable, step-by-step manner.
Summary
- SOLID principles form the foundation for all pattern applications in architect-awesome, ensuring that Singleton, Strategy, and other patterns do not violate encapsulation or create tight coupling.
- Creational patterns (Singleton, Factory, Builder) manage object lifecycle and configuration, critical for database connections and service initialization.
- Behavioral patterns (Chain-of-Responsibility, Strategy, Observer, Command) decouple request handling, algorithm selection, and event propagation, enabling microservices to scale without cascading changes.
- Structural patterns (Proxy, Adapter, Decorator, Facade) simplify integration with external systems and add cross-cutting functionality like caching and logging without modifying core business logic.
- The repository’s
README.mdserves as the canonical reference, mapping each pattern to concrete backend scenarios from servlet filters to DAO factories.
Frequently Asked Questions
What are the most critical design patterns for microservices backend architecture?
Singleton, Factory, and Observer rank among the most critical for microservices. Singleton ensures single instances of configuration managers and connection pools, preventing resource exhaustion. Factory abstracts the creation of service clients and DAOs, allowing services to switch between database vendors or message brokers without code changes. Observer enables event-driven communication between services via domain events, decoupling producers from consumers and supporting eventual consistency across distributed systems.
How does the Chain-of-Responsibility pattern improve HTTP request handling?
The Chain-of-Responsibility pattern improves HTTP handling by decoupling authentication, logging, compression, and business logic into discrete, reusable filters. As documented in the repository’s reference to javax.servlet.Filter#doFilter(), each filter processes the request and optionally passes it to the next handler in the chain. This structure allows developers to add, remove, or reorder processing steps—such as inserting a rate-limiting filter between authentication and routing—without modifying existing filter code or the core controller logic.
Why are SOLID principles prerequisites for applying design patterns in backend systems?
SOLID principles act as guardrails that prevent pattern misuse from creating architectural debt. For example, without the Single Responsibility Principle, a Singleton could evolve into a "god object" managing configuration, caching, and business logic simultaneously. The Dependency Inversion Principle ensures that Factory and Strategy patterns depend on abstractions rather than concrete classes, enabling testability with mocks. By adhering to SOLID fundamentals first—as emphasized in the repository’s principles section—backend developers ensure that patterns like Proxy, Observer, and Command enhance rather than complicate the architecture.
Where does Aspect-Oriented Programming (AOP) fit into backend design patterns?
AOP addresses cross-cutting concerns—functionality that spans multiple layers such as logging, transaction management, and security—without scattering code through business classes. According to the repository’s AOP section, this pattern complements core design patterns by modularizing orthogonal concerns. For example, while a PaymentService uses Strategy to select payment methods, AOP can wrap the service to manage database transactions and audit logging, keeping the business logic pure and focused solely on payment processing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →