Software Architecture
Design Principles
Core principles for making sound architectural decisions — from modularity to SOLID, coupling to abstraction.
Introduction to Design Principles
Inputs (WHAT?)
- Refined Requirements
- Quality Goals
- Design Constraints
- Influencing Factors
Outputs (HOW?)
- High-Level Design — Overall structure, architecture, subsystems, interfaces…
- Low-Level Design — Component design, data structures…
Design principles are generalized knowledge that (often) helps to make design decisions. They are proven conventions and best practices.
Reduce Unwanted Complexity
Simplify the system wherever possible — complexity kills maintainability and increases error probability.
Achieve Quality Requirements
Increase flexibility, changeability, and adaptability to meet current and future demands.
Information Hiding, SoC & Modularity
Developed by David L. Parnas — encapsulating complexity in components improves flexibility, stability, testability, and understandability.
Black Box Approach
Treat components as black boxes. Hide internal details from clients. No direct access to internal data.
Defined Interfaces Only
Access only via defined interfaces. Expose only what is needed, hide everything else.
Breaking Encapsulation
Breaking encapsulation leads to unwanted dependencies throughout the system.
In Java: private variables + public methods = Information Hiding in practice.
Objective: Manage different problems separately. Separate concerns by domain, sub-domains, sub-tasks; persistence, logic, behavior, presentation. Split complex systems according to responsibilities.
SoC in practice — each concern (user management, product catalog, order processing) is owned by an independent service and team. Services communicate through well-defined interfaces but remain internally autonomous.
SoC evolution in three stages — from a single monolithic class, to focused domain classes, to a fully layered presentation + domain architecture.
─ passengerId: String
─ reservationId: String
─ paymentAmount: double
+ bookFlight(): void
+ cancelReservation(): void
+ getPassengerInfo(): void
+ processPayment(): boolean
+ displayBookingUI(): void
+ renderConfirmation(): void
+ saveToDB(): void
+ sendEmailConfirmation(): void
─ departure: Date
+ bookSeat(): void
+ getAvailableSeats(): int
+ cancel(): void
─ status: String
+ create(): void
+ cancel(): void
+ getStatus(): String
─ email: String
+ getInfo(): String
+ updateProfile(): void
+ validate(): boolean
─ method: String
+ process(): boolean
+ refund(): void
+ getReceipt(): String
+ showConfirmation(): void
+ renderPassengerList(): void
+ cancel(): void
+ cancel(): void
+ validate(): boolean
+ refund(): void
SoC is not just a code-level principle — it appears at every layer of a system, from silicon to cloud. Each level separates concerns to manage complexity and enable independent evolution.
Low-Level Embedded Systems
Hardware I/O, HAL, RTOS scheduling, and application logic are kept as separate concerns — each layer hides its complexity from the one above it.
Drivers in an Operating System
Each driver handles one device concern. The kernel exposes a uniform interface — devices are independently developed and swappable without touching other parts of the OS.
Network in Cloud Computing
Cloud networking separates physical infrastructure, compute, network configuration, and application concerns into independent layers — each managed by different teams and tools.
Container Orchestration in Hybrid Cloud
Kubernetes separates application deployment concerns from infrastructure concerns — the same workloads run on-premises or in the cloud with no application code changes.
Encapsulate
Modules encapsulate responsibilities. Each module has a clear, single purpose.
Expose Interfaces
Expose only well-defined interfaces. Internal details remain hidden.
Independent Development
Can be developed, maintained, and tested independently. Email and SMS as separate modules.
- → Hides the internal details from clients
- → Allows access only through defined interfaces
- → Split complex systems according to responsibilities
- → Reduce the complexity of each component
- → We can build complex systems from smaller modules
- → Allows us to replace modules
Loose Coupling
Definition: Degree or measure of how closely two components are connected. Components inherently interact with each other — this is a necessary and inevitable property. Each dependency and relationship increases complexity.
Call
Block A calls Block B → A depends on B. Cyclical relationships are a special type.
Message
Sender → Message Broker → Receiver. More loose than direct calls.
Creation
Creator instantiates an Object (e.g., new SomeClass(10)). Creator depends on created class.
Data Structure
Clients share knowledge of a data structure. Both depend on it. Indirect dependency.
Time
Successful execution of one component depends on timing of another. Sequential, scheduling, or real-time constraints.
Execution Location
Components must run on the same OS/VM/hardware. Microservice instance vs. logging process on same machine.
Inheritance
Child extends Parent — constructors, methods, attributes all inherited. A very strong type of coupling.
Sequential Dependency
One process must complete before another can start. Example: transactions in a Financial Trading Platform.
Scheduling Dependency
Tasks must execute at specific times or intervals. Example: Batch process jobs / push notifications.
Real-Time Constraints
Processes must respond within strict time limits. Example: Video frames in a Video Streaming System.
- Few, well-defined dependencies
- Well-structured relationships
- Explicit dependencies only
- Easier to understand
- Easier to re-use components
- Easier to replace, more flexible
- Changes are local, not global
- Many implicit dependencies
- Tangled relationships
- Side effects on changes
- Hard to understand
- Hard to reuse
- Hard to replace components
- Changes ripple globally
Goal: Few, well-defined, well-structured, and explicit dependencies.
- Definition: "Degree or measure of how closely two components are connected."
- 7 Types of Coupling:
- Call
- Message
- Creation
- Shared Data / Data Structure
- Time
- Execution Location
- Inheritance
- Achieving loose coupling:
- Keep dependencies simple
- Keep the number of dependencies low
- Benefits of loose coupling:
- Easier to understand the system
- Easier to reuse components
- Easier to replace components
- Changes are local, not global
High Cohesion
Definition: Degree to which elements of a building block, component or module belong together.
High Cohesion
The functionality of elements within a component is strongly related to each other. All elements contribute to one clear purpose.
Low Cohesion
Elements that aren't related to each other reside in the same system component. The component has multiple unrelated responsibilities.
Coupling = dependency between modules · Cohesion = dependency within a module
Low Cohesion, High Coupling
Product & User Service bundles: Product Listing + Authentication & Authorization + User Profiles — unrelated concerns in one service. Results in very few interactions between services but high coupling within (confirm stock / request shipping).
High Cohesion, Low Coupling
Separate services: Product Management (Listing + Stock), User Management (Auth + Profiles), Order Management (Processing + Shipping), Checkout Service (Validation + Payment). Related things stay together.
Cohesion depends on context and is not always obvious. It is hard to measure directly. Criteria for cohesion depend on context — it is always an explicit design decision.
- Cohesion: "The degree to which elements of a building block, component or module belong together."
- High Cohesion: "The functionality of elements within a component is strongly related to each other."
- Low Cohesion → High Coupling
- High Cohesion → Low Coupling
- Challenges of:
- Achieving high cohesion
- Measuring cohesion
KISS, YAGNI & DRY
Keep It Simple, Stupid
Goal: prefer simple, less complex solutions. Complexity kills systems — simpler is easier to comprehend, maintain, and less error-prone.
YAGNI
You Aren't Going to Need It. Don't add features or flexibility "just in case." Build only what is needed now.
Challenges
🔮 Inability to Predict the Future
- If the design is too simple (not flexible enough), future changes may be difficult to make
- If the design is too flexible, the system will be extra complex
⚡ Simplicity vs Performance
- Performance optimizations may make the design more complex
- Consider the necessity of those optimizations!
- If they are necessary, make the compromise and prefer performance
- If they are not necessary, wait for Moore's law and prefer simplicity
Make things as simple as possible, but not simpler.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
Avoid duplicating code, data, or responsibilities. Duplication only when wanted/required to minimize complexity. NO DRY = WET (Waste Everyone's Time).
- 3 separate Payment Logic modules
- Bug in one = fix all three
- Duplicated work & testing
- Error prone
- Hard to maintain
- Central Payment Processor
- Single source of truth
- Changes are easy to make
- Fewer tests needed
- Easy to understand
- KISS — Keep It Simple and Stupid
- YAGNI — You Aren't Going to Need It
- Main motivator:
- Easier to understand and maintain
- Less error prone
- Main motivator:
- DRY — Don't Repeat Yourself
- Avoid duplication of:
- Code
- Data
- Responsibility
- May increase coupling
- Avoid duplication of:
Murphy's Law & Postel's Law
"Anything that can go wrong will go wrong."
In production environments, we have no control over the environment or network. Servers crash, cloud providers fail, mobile clients misbehave, connected cars lose signal. Design defensively from the start.
Things will break
If it can go wrong,
it will go wrong
Timeout, OOM,
null, race condition
Anticipate Errors
- What do I depend on?
- What can possibly go wrong?
- What are failure modes?
Contain the Error
- Checking error status
- Exception handling
- Throttling
- Back-pressure
Support Error Analysis
- Logging
- Metrics (request/rate, error rate)
- Dashboards
- Distributed tracing
"Be conservative in what you do, be liberal in what you accept from others."
Be precise and strict when designing your components, but:
- Tolerate errors in other components
- Be able to recover from errors
- Degrade gracefully
Accept slightly non-strict format from others, but always send the strict format yourself (e.g., to a database).
- Backward compatibility
- Tolerates API version drift (v2.0 → v3.0)
- Graceful degradation
- Recovers from partial failures
- High complexity (legacy code, edge cases)
- Technical debt accumulation
- System harder to understand
- Performance overhead (validating many formats)
- Expect Errors / Murphy's Law
"Anything that can go wrong, will go wrong"- Anticipate errors
- Contain errors
- Support analysis of errors
- Robustness Principle — Postel's Law
- Be strict when designing our component
- Tolerate errors from users/other components
Abstraction & Conceptual Integrity
Identify Useful Generalizations
Emphasize common properties or functionalities. Omit/hide unnecessary details. Group Dog, Cat, Turtle under the abstraction Pet.
Depend on Abstractions
Depend only on abstractions, not on implementations. Complements SoC, DRY, and Information Hiding.
API Gateway
Abstracts the entire application from the client. A single GET /search?q="movies" hides the Search Service, Autocompletion, CDC, Map/Reduce, and Key-Value stores behind it.
OOP Interfaces
PaymentProcessor interface abstracts CreditCardProcessor, PayPalProcessor, BankTransferProcessor. CheckoutService and OrderManagement depend only on the abstraction.
Operating System
OS abstracts CPU, hardware drivers, memory scheduling. Applications never interact with raw hardware.
Frontend Frameworks
React/Vue/Angular abstract HTML, CSS, and JavaScript. Write components, not browser APIs.
ORM
Hibernate, Entity Framework abstract SQL and relational databases. Work with objects, not raw queries.
IaC
Terraform / CloudFormation abstract physical hardware and cloud infrastructure into declarative code.
…conceptual integrity is the most important consideration in system design. It is better to have a system omit certain features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas.
Goal: Clear, Recognizable Concepts
Critical for creating understandable, maintainable, and scalable systems. Lower learning curve. Reduces element of surprise.
Examples
- Unix — everything is a file
- Lisp — everything is a list
- Smalltalk — everything is an object
- Haskell/Erlang — everything is immutable
🐧 Unix — "Everything is a File"
🔗 Lisp — "Everything is a List"
Lower Learning Curve
Easier and faster for developers to:
- Understand the architecture
- Become productive
Helps Testers Detect
- Errors
- Deviations from core concepts
Reduces the Element of Surprise
Consistent concepts mean fewer unexpected behaviors — the system becomes less error-prone.
- Abstraction
- Generalizations of common properties/functionalities
- Hide details
- Depend only on abstractions
- Conceptual Integrity
- Creates clear, consistent and recognizable concepts/abstractions
- Reduces learning curve
- Makes the system:
- More understandable
- Less error-prone
SOLID Principles
Introduced by Robert C. Martin in Design Principles and Design Patterns. Mainly intended for OOP design — some can be extended to other fields of software architecture.
Responsibility
Substitution
Segregation
Inversion
Each component is responsible for only one clearly defined task.
Encapsulate and contain only functions or sub-components directly contributing to that task.
"A class should have only one reason to change."
Related to: Separation of Concerns (SoC)
- Will increase cohesion
- Can decrease coupling
Example: Splitting MonolithicReportGenerator — any change to DB or format requires a full class change — into separate modules:
Each module is separate — changes only affect that module.
Monolith (Violation)
One class handles: retrieve, validate, process, format, and send reports. Any database or format change requires full class modification.
SRP Applied
- DataRetriever
- ReportValidator
- DataProcessor
- ReportFormatter
- ReportSender
SRP in Practice
- Methods: filter, write, read
- DB tables: Users, Products, Orders
- REST: /users, /orders, /comments
- Packages: net.http, db.mysql
- Microservices: Images, Notification
Components should be open for extension but closed for modification.
- Extend behavior and features without changes to source code
- No "side effects" from future extensions
- Changes do not require modifications to existing users
"Software entities should be open for extension, but closed for modification."
Device Drivers (OS)
Linux Kernel is closed for modification. New device drivers (Keyboard, Mouse, USB, Network) extend it without changing the kernel.
IDE Plugins
IntelliJ IDEA is closed for modification. New plugins (JUnit, TestNG, GitHub) extend it without changing the IDE core.
An object of a superclass should be replaceable with objects of its subclasses — without:
- Any surprises
- Any side effects
- Any additional setups or clean-up procedures
"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program."
LSP Satisfied
EngineeringReport, BusinessReport all implement Report correctly. ReportPrinter can use any of them interchangeably.
LSP Violated — FinancialReport
FinancialReport is more restrictive (throws unauthorized exception) and requires additional setup (setPassword). Caller must add an instanceof check — tight coupling and side effects.
Clients should not be forced to depend on methods they do not use.
- Multiple small and specific interfaces are better than a single, large one
- Split large interfaces by semantics or by responsibility → "role interface"
- Promotes loose coupling
- Improves maintainability and changeability
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details (implementations) should depend on abstractions.
High-Level Modules
Should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
Abstractions
Should not depend on concrete implementations. Implementations should depend on abstractions.
Low-Level Modules
All concrete implementations should depend on abstractions, not the other way around.
| Principle | Core Statement | Key Benefit |
|---|---|---|
| SRP — Single Responsibility | One class = one reason to change | Increases cohesion, decreases coupling |
| OCP — Open-Closed | Open for extension, closed for modification | Extend behavior without changing existing code |
| LSP — Liskov Substitution | Subtypes must be substitutable without surprises | Reliable polymorphism, no instanceof hacks |
| ISP — Interface Segregation | Clients shouldn't depend on methods they don't use | Loose coupling, smaller focused interfaces |
| DIP — Dependency Inversion | High-level & low-level both depend on abstractions | Decoupled, swappable implementations |
- SRP (Single Responsibility Principle):
- Each component should be responsible for only one clearly defined task
- Encapsulate all the functions or sub-components directly contributing to this task
- OCP (Open-Closed Principle):
- Components should be open for extension but closed for modification
- LSP (Liskov Substitution Principle):
- Subtypes must be substitutable for their base types without surprises
- ISP (Interface Segregation Principle):
- Clients should not be forced to depend on methods they do not use
- We should make interfaces smaller and tailor them to each group of clients
- DIP (Dependency Inversion Principle):
- Instead of high-level modules depending on lower-level modules
- → both high-level and low-level modules depend only on abstractions
Why Design Principles?
- Guiding rules for structuring software
- Reduce complexity & improve maintainability
- Apply at component, module & system level
Foundational Trio
- Information Hiding — hide internals behind stable interfaces
- SoC — separate business logic from technology concerns
- Modularity — encapsulated units with clear public APIs
Minimize Dependencies
- 7 types: call, message, creation, shared data, time, location, inheritance
- Keep dependencies simple & few
- Enables independent change & deployment
Related Things Together
- Elements within a component should be strongly related
- High cohesion → low coupling
- Challenging to achieve & measure in practice
Simplicity & No Duplication
- KISS — keep solutions simple and understandable
- YAGNI — don't build what you don't need yet
- DRY — avoid duplicating code, data & responsibility
Expect Errors, Be Robust
- Murphy's Law — anything that can go wrong, will
- Anticipate, contain & support analysis of errors
- Postel's Law — be strict in output, tolerant in input
Generalize & Stay Consistent
- Abstraction — hide details, depend on generalizations
- Conceptual Integrity — consistent, recognizable concepts
- Reduces learning curve, improves understandability
Five OO Design Rules
- SRP — one responsibility per component
- OCP — open for extension, closed for modification
- LSP — subtypes substitutable for base types
- ISP — small, client-specific interfaces
- DIP — depend on abstractions, not concretions