System design is the blueprint for building scalable, reliable, and maintainable software. Understanding the underlying design principles and applying well‑known patterns enables engineers to create systems that meet business goals while staying adaptable to future changes.
Fundamental Design Principles
The following principles form the foundation of any robust architecture. They are not mutually exclusive; rather, they complement each other.
- Separation of Concerns (SoC)
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
- Don't Repeat Yourself (DRY)
- KISS – Keep It Simple, Stupid
- YAGNI – You Aren’t Gonna Need It
Separation of Concerns (SoC)
SoC encourages dividing a system into distinct sections, each handling a specific aspect of functionality. This reduces coupling and makes each module easier to understand, test, and replace.
Single Responsibility Principle (SRP)
A class or module should have one reason to change. When responsibilities are mixed, a change in one area may unintentionally affect another, leading to brittle code.
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. This is typically achieved through abstraction layers such as interfaces or abstract base classes.
Liskov Substitution Principle (LSP)
Derived types must be substitutable for their base types without altering the correctness of the program. Violations often manifest as runtime exceptions or subtle bugs.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use. Fine‑grained interfaces prevent the “fat interface” anti‑pattern.
Dependency Inversion Principle (DIP)
High‑level modules must not depend on low‑level modules; both should depend on abstractions. This principle is the backbone of Inversion of Control containers.
Common Design Patterns
Design patterns provide reusable solutions to recurring architectural problems. Below is a concise taxonomy of patterns most relevant to system design.
- Creational: Singleton, Factory Method, Abstract Factory, Builder, Prototype
- Structural: Adapter, Bridge, Composite, Decorator, Facade, Proxy
- Behavioral: Observer, Strategy, Command, Iterator, State, Template Method
Example: Builder Pattern for Configurable Objects
The Builder pattern separates object construction from its representation, allowing the same construction process to create different representations.
public class HttpClient {
private final String baseUrl;
private final int timeout;
private final boolean followRedirects;
private HttpClient(Builder builder) {
this.baseUrl = builder.baseUrl;
this.timeout = builder.timeout;
this.followRedirects = builder.followRedirects;
}
public static class Builder {
private String baseUrl;
private int timeout = 30; // default seconds
private boolean followRedirects = true;
public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public Builder timeout(int seconds) {
this.timeout = seconds;
return this;
}
public Builder followRedirects(boolean flag) {
this.followRedirects = flag;
return this;
}
public HttpClient build() {
if (baseUrl == null) {
throw new IllegalStateException("baseUrl is required");
}
return new HttpClient(this);
}
}
}
// Usage
HttpClient client = new HttpClient.Builder()
.baseUrl("https://api.example.com")
.timeout(60)
.followRedirects(false)
.build();
The Builder pattern shines when an object has many optional parameters or when the construction process involves several steps that can vary independently.
Applying Principles & Patterns Together
Effective system design emerges when principles guide the selection and composition of patterns. The table below illustrates typical scenarios, the guiding principle, and the recommended pattern.
| Scenario | Guiding Principle | Recommended Pattern(s) |
|---|---|---|
| Creating complex immutable objects | SRP + OCP | Builder, Factory Method |
| Integrating third‑party services without leaking dependencies | DIP | Adapter, Facade |
| Implementing pluggable business rules | OCP + ISP | Strategy, Command |
| Decoupling UI from business logic | SoC | Mediator, MVC |
| Managing cross‑cutting concerns (logging, auth) | DIP | Decorator, Proxy |
Practical Guidelines
- Start with clear domain boundaries before picking patterns.
- Prefer composition over inheritance to respect the Open/Closed Principle.
- Keep interfaces minimal; split them when they start to grow (ISP).
- Document the intent of each pattern – future maintainers often forget why a pattern was introduced.
- Avoid premature optimization; apply YAGNI and KISS.
Q: When should I choose the Facade pattern over Adapter?
A: Use Facade when you want to provide a simplified interface to a complex subsystem for client convenience. Use Adapter when you need to make an existing component conform to a new interface without altering its internals.
Q: Is the Singleton pattern still relevant in modern microservice architectures?
A: Singletons are generally discouraged for shared state across services because they introduce hidden global state. In a microservice world, prefer dependency injection containers or external configuration stores.
Q: How do I measure whether a pattern adds value?
A: Track metrics such as code readability, test coverage, and change‑impact analysis. If a pattern reduces the number of files modified per change or improves testability, it likely adds value.
Q. Which principle encourages writing code that is open for extension but closed for modification?
- SRP
- OCP
- LSP
- ISP
Answer: OCP
The Open/Closed Principle states that software entities should be extensible without altering existing code.
Q. Which design pattern is most appropriate for constructing objects with many optional parameters?
- Factory Method
- Builder
- Prototype
- Adapter
Answer: Builder
The Builder pattern separates construction logic from representation, handling numerous optional parameters cleanly.