LearningTree

Software Architecture
Design Principles

Core principles for making sound architectural decisions — from modularity to SOLID, coupling to abstraction.

01
Chapter One

Introduction to Design Principles

From "WHAT?" to "HOW?"

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…
Purpose & General Objective

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.

Rule #1: Know When to Break the Rules You are responsible for fulfillment of requirements and solution of the problem at hand — not for perfect adherence to principles. Principles can contradict each other. Weigh specific requirements against principles. Know the long-term consequences of violating them. Make deliberate decisions.
02
Chapter Two

Information Hiding, SoC & Modularity

Information Hiding Principle

Developed by David L. Parnas — encapsulating complexity in components improves flexibility, stability, testability, and understandability.

📦

Black Box Approach

Caller call hidden internals BLACK BOX result

Treat components as black boxes. Hide internal details from clients. No direct access to internal data.

🔌

Defined Interfaces Only

MODULE private state I/F Client ✗ direct access blocked

Access only via defined interfaces. Expose only what is needed, hide everything else.

⚠️

Breaking Encapsulation

MODULE field1 field2 field3 Caller A Caller B ✗ direct coupling → unwanted dependencies

Breaking encapsulation leads to unwanted dependencies throughout the system.

In Java: private variables + public methods = Information Hiding in practice.

Information Hiding — Application + Library Example
Video Streaming Application Application </> uses Video Encoding Library Encoding API ► passes video frames Codec ► processes Video Processor Hardware Abstraction Layer ▼ utilizes ▼ utilizes Hardware GPU CPU internals hidden from caller
Separation of Concerns (SoC)

Objective: Manage different problems separately. Separate concerns by domain, sub-domains, sub-tasks; persistence, logic, behavior, presentation. Split complex systems according to responsibilities.

SoC — Business Logic vs Technology
Business Logic
Domain Model
Business Rules
Semantics
SEPARATE
Technology
Persistence
REST API
Dependency Injection

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 — High-Level Service Example (E-Commerce Platform)
User User Service Auth · Profiles · Sessions Product Service Catalog · Inventory · Search Order Service Cart · Checkout · Billing Team Alpha Team Beta Team Gamma Each service owns its concern independently — teams deploy, scale & evolve without coupling

SoC evolution in three stages — from a single monolithic class, to focused domain classes, to a fully layered presentation + domain architecture.

SoC Evolution — FlightReservationSystem
① Monolith
FlightReservationSystem
─ flightId: String
─ passengerId: String
─ reservationId: String
─ paymentAmount: double
+ bookFlight(): void
+ cancelReservation(): void
+ getPassengerInfo(): void
+ processPayment(): boolean
+ displayBookingUI(): void
+ renderConfirmation(): void
+ saveToDB(): void
+ sendEmailConfirmation(): void
✗ Business logic, persistence, UI & payment all tangled in one class
split
② Domain Separation
Flight
─ flightId: String
─ departure: Date
+ bookSeat(): void
+ getAvailableSeats(): int
+ cancel(): void
Reservation
─ reservationId: String
─ status: String
+ create(): void
+ cancel(): void
+ getStatus(): String
Passenger
─ passengerId: String
─ email: String
+ getInfo(): String
+ updateProfile(): void
+ validate(): boolean
Payment
─ amount: double
─ method: String
+ process(): boolean
+ refund(): void
+ getReceipt(): String
✓ Domain logic separated — but UI & persistence still scattered inside domain classes
extract UI
③ Presentation + Domain
Presentation Layer
<<UI>> ReservationUI
+ displayBookingForm(): void
+ showConfirmation(): void
+ renderPassengerList(): void
↓  uses
Domain Layer
Flight
+ bookSeat(): void
+ cancel(): void
Reservation
+ create(): void
+ cancel(): void
Passenger
+ getInfo(): String
+ validate(): boolean
Payment
+ process(): boolean
+ refund(): void
✓ Presentation fully separated from domain — clean layer boundaries enforced
SoC at Other Levels of Abstraction

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 Sensors / GPIO HAL Hardware Abstraction RTOS Scheduling / Tasks App

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

OS KERNEL (User Applications) Network Driver TCP/IP stack GPU Driver render pipeline Storage Driver block I/O USB Driver protocol

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

Application Layer — Services & APIs Network Layer — VPC · Subnets · Routing Compute Layer — VMs · Containers · Functions Physical / Data-Centre Infrastructure

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

Orchestration Plane (Kubernetes) On-Premises Cluster Pod A Pod B · Pod C Public Cloud Cluster Pod D Pod E · Pod F

Kubernetes separates application deployment concerns from infrastructure concerns — the same workloads run on-premises or in the cloud with no application code changes.

Modularity = SoC + Information Hiding
🧩

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.

Example 1 — Email & SMS as Separate Encapsulated Modules
NotificationService send(message, channel) uses uses EMAIL MODULE «interface» IEmailSender + sendEmail(to, subject, body): void SMTPClient «hidden» TemplateEngine «hidden» SMS MODULE «interface» ISmsSender + sendSms(phone, message): void TwilioClient «hidden» RetryHandler «hidden»
Example 2 — Library as a Module (encapsulated internals, public API)
Application your code « consumer » uses API 📚 JAVA COLLECTIONS LIBRARY PUBLIC API (Interfaces) List<T> · Map<K,V> · Set<T> · Queue<T> · Iterator<T> ArrayList add() / remove() get() / size() internal array[] resize logic «hidden» HashMap put() / get() remove() / keys() hash buckets[] collision chain «hidden» TreeSet add() / contains() first() / last() Red-Black Tree balancing logic «hidden» LinkedList add() / poll() peek() / offer() Node<T> chain pointer logic «hidden»
Example 3 — Microservices as Independent Modules
API Gateway routes · auth · rate-limit User Service «REST API» POST /register POST /login GET /profile/{id} JWT Auth · Bcrypt User Repository « encapsulated » Product Service «REST API» GET /products GET /products/{id} POST /products/search Catalog Engine Inventory Tracker « encapsulated » Order Service «REST API» POST /orders GET /orders/{id} PUT /orders/{id}/cancel Payment Gateway Fulfilment Logic « encapsulated » Notification Svc «Event Consumer» on OrderPlaced on UserRegistered on PaymentFailed Email + SMS Push Provider « encapsulated » OrderPlaced event
Information-Hiding Principle
  • Hides the internal details from clients
  • Allows access only through defined interfaces
Separation of Concerns (SoC)
  • Split complex systems according to responsibilities
  • Reduce the complexity of each component
Modularity = Information-Hiding + SoC
  • We can build complex systems from smaller modules
  • Allows us to replace modules
03
Chapter Three

Loose Coupling

What is 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.

Coupling
Dependencies
Complexity
7 Types of Coupling
📞

Call

Block A caller calls Block B callee A depends on B

Block A calls Block B → A depends on B. Cyclical relationships are a special type.

✉️

Message

Sender Message Broker queue Receiver loosely coupled via broker

Sender → Message Broker → Receiver. More loose than direct calls.

🏗️

Creation

Creator new Obj() «creates» SomeClass instance creator knows constructor

Creator instantiates an Object (e.g., new SomeClass(10)). Creator depends on created class.

🌲

Data Structure

Client #1 Dependency (shared struct) Client #2 knows structure knows structure bag with 23 ~ 18,014 elements Depends on Depends on indirect dependency — like db read/write consistency

Clients share knowledge of a data structure. Both depend on it. Indirect dependency.

⏱️

Time

Step A must finish Step B ⏳ waiting execution order / timing dependency

Successful execution of one component depends on timing of another. Sequential, scheduling, or real-time constraints.

🗺️

Execution Location

SAME MACHINE / VM Microservice instance local Logger process

Components must run on the same OS/VM/hardware. Microservice instance vs. logging process on same machine.

🧬

Inheritance

Animal «parent» Dog extends — inherits all ⚠ strongest coupling

Child extends Parent — constructors, methods, attributes all inherited. A very strong type of coupling.

Time Coupling Details
🔗

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.

Achieving Loose Coupling
✓ Loose Coupling (Goal)
✗ Strong Coupling (Avoid)
  • 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.

— Loose Coupling Principle
📋 Coupling — Summary
  • 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
04
Chapter Four

High Cohesion

What is 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.

Cohesion vs Coupling Relationship
HIGH COHESION
LOW COUPLING
LOW COHESION
TIGHT COUPLING

Coupling = dependency between modules · Cohesion = dependency within a module

Online Store Example — Low vs High Cohesion

Low Cohesion, High Coupling

Product & User Service Product Listing Authentication & Authorization User Profiles ▼ Queries stock ▼ Verifies for payment ▼ Details for order Order & Checkout Service Stock Management Payment Processing Order Processing ◄ Confirms stock for validation ▼ Validates payment Shipping & Management Service Order Validation Shipping Management ▼ Confirms order for shipping ▶ Requests shipping

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

Product Management Service Product Listing Stock Management ▼ Validates availability ▲ Updates based on orders Checkout Service Order Validation ▼ Confirms order details Payment Processing User Management Service Authentication & Authorization User Profiles ◄ Confirms user identity Order Management Service Order Processing ▼ Arranges shipping Shipping Management

Separate services: Product Management (Listing + Stock), User Management (Auth + Profiles), Order Management (Processing + Shipping), Checkout Service (Validation + Payment). Related things stay together.

High Cohesion — Challenges
A B C C A B B C A Criteria for cohesion depend on context. Explicit design decision! A B C C A B B C A ? A B C A B C A B C ? A B C C A B B C A ?

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.

📋 High Cohesion — Summary
  • 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
05
Chapter Five

KISS, YAGNI & DRY

KISS — Keep It Simple
💋

Keep It Simple, Stupid

COMPLEX SIMPLE

Goal: prefer simple, less complex solutions. Complexity kills systems — simpler is easier to comprehend, maintain, and less error-prone.

🚫

YAGNI

WHAT YOU BUILD ✓ What you need maybe? just in case future-proofing ✗ BUILD THIS ✓ Need now

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.

— Albert Einstein

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

— Antoine de Saint-Exupéry
DRY — Don't Repeat Yourself

Avoid duplicating code, data, or responsibilities. Duplication only when wanted/required to minimize complexity. NO DRY = WET (Waste Everyone's Time).

✗ Without DRY (WET)
Service A PaymentLogic Service B PaymentLogic Service C PaymentLogic 🐛 Bug? Fix all 3! fix ✗ fix ✗ fix ✗
  • 3 separate Payment Logic modules
  • Bug in one = fix all three
  • Duplicated work & testing
  • Error prone
  • Hard to maintain
✓ With DRY
Payment Processor single source of truth Service A Service B Service C Bug? Fix once ✓
  • Central Payment Processor
  • Single source of truth
  • Changes are easy to make
  • Fewer tests needed
  • Easy to understand
⚠️ DRY Limitation in Microservices DRY cannot always be applied to microservices — forcing shared databases, libraries, or languages would restrict independence and add more coupling between services, defeating the purpose of microservices architecture.
📋 KISS, YAGNI & DRY — Summary
  • KISS — Keep It Simple and Stupid
  • YAGNI — You Aren't Going to Need It
    • Main motivator:
      • Easier to understand and maintain
      • Less error prone
  • DRY — Don't Repeat Yourself
    • Avoid duplication of:
      • Code
      • Data
      • Responsibility
    • May increase coupling
06
Chapter Six

Murphy's Law & Postel's Law

Expect Errors — Murphy'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.

Production — Things That Go Wrong
🖥️ Server Crash 💥 ☁️ Cloud Provider Fail ⚠️ 🌐 Network Loss 🔌 📱 Mobile Client Misbehaves 🐛 🚗 Connected Car Signal Lost 📡 DESIGN DEFENSIVELY FROM THE START
Expect: Errors → Murphy's Law → Examples
🚨 Errors
Things will break
⚖️ Murphy's Law
If it can go wrong,
it will go wrong
📋 Examples
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
Robustness Principle — Postel's Law

"Be conservative in what you do, be liberal in what you accept from others."

— Jon Postel (RFC 793)

Be precise and strict when designing your components, but:

  • Tolerate errors in other components
  • Be able to recover from errors
  • Degrade gracefully
⚠️ This does not come for free! There are costs and risks that come with adhering to Postel's Law.

Accept slightly non-strict format from others, but always send the strict format yourself (e.g., to a database).

Benefits
Downsides
  • 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)
Postel's Law — Examples
Example 1 — Accept Non-Strict, Send Strict
👤📱 Mobile User API DB "user": { "id": "12345", "name": "Jane Doe", } "status": "added", ↑ partial / non-strict "user": { "id": "12345", "name": "Jane Doe", "email": "jane@example.com", "isActive": "true", "roles": ["user","admin"], } ↑ strict / complete format to DB Server accepts partial input from client, but stores complete strict format in database
Example 2 — Backward Compatibility (API Version Drift)
👤📱 Mobile User HTTP Request API: v2.0 </> Gateway API: v3.0 HTTP Request API: v3.0 DB API: v3.0 🔑 Backward Compatibility Gateway accepts v2.0 from client, translates & forwards as v3.0 internally
📋 Murphy's Law & Postel's Law — Summary
  • 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
07
Chapter Seven

Abstraction & Conceptual Integrity

Abstraction Design Principle
Abstraction — Hide Details, Depend on Generalizations
🐕 Dog 🐈 Cat 🐢 Turtle «interface» Pet Group common properties → omit details Consumer CheckoutService depends on CreditCard not implementations
🔭

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.

Examples of Abstractions
🌐

API Gateway

👤 API Gateway Search Service Autocomplete Map/Reduce Key-Value Store single entry point hides all

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

«interface» PaymentProcessor CreditCard PayPal BankTransfer CheckoutService OrderMgmt

PaymentProcessor interface abstracts CreditCardProcessor, PayPalProcessor, BankTransferProcessor. CheckoutService and OrderManagement depend only on the abstraction.

🖥️

Operating System

App A App B App C Operating System abstraction layer 🔧 CPU 💾 Memory 🖨️ Drivers apps never touch raw hardware

OS abstracts CPU, hardware drivers, memory scheduling. Applications never interact with raw hardware.

⚛️

Frontend Frameworks

<Button/> <UserProfile/> React / Vue / Angular framework abstraction HTML CSS DOM APIs write components, not browser APIs

React/Vue/Angular abstract HTML, CSS, and JavaScript. Write components, not browser APIs.

🗄️

ORM

User.java Order.java Hibernate / EF ORM abstraction 🗄️ SQL / Relational Database work with objects, not raw queries

Hibernate, Entity Framework abstract SQL and relational databases. Work with objects, not raw queries.

☁️

IaC

main.tf template.yaml Terraform / CloudFormation IaC abstraction ☁️ VMs 🌐 VPC 🗄️ S3 🔒 IAM declarative code, not manual setup

Terraform / CloudFormation abstract physical hardware and cloud infrastructure into declarative code.

Conceptual Integrity

…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.

— Fred Phillips Brooks, Software Architecture in Practice
🎯

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"

PERMISSIONS LINKS OWNER SIZE DATE NAME drwxr-xr-x 5 root root 4096 Oct 1 12:00 /bin/ # Directory -rw-r--r-- 1 user user 52 Sep 30 11:00 /home/user/document.txt # Regular file lrwxrwxrwx 1 user user 4 Oct 1 12:30 /home/user/link -> document.txt # Symbolic link crw-rw-rw- 1 root tty 5, 1 Sep 28 14:00 /dev/tty1 # Character device file brw-rw---- 1 root disk 8, 0 Sep 14 10:00 /dev/sda # Block device file prw-r--r-- 1 user user 0 Sep 13 09:00 /home/user/pipe # Named pipe (FIFO) srw-rw-rw- 1 root root 0 Oct 1 08:00 /var/run/socket # Socket drwxr-xr-x 2 root root 4096 Oct 1 14:00 /home/user/dir # Another directory directories, devices, pipes, sockets — all accessed as files

🔗 Lisp — "Everything is a List"

( defun sum-list ( lst ) ( if ( null lst ) 0 ( + ( car lst ) ( sum-list ( cdr lst ))))) ; code = list ; data = list ; function call = list ; (car lst) → first element of list ; (cdr lst) → rest of the list
Conceptual Integrity — Benefits
📉

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 & Conceptual Integrity — Summary
  • 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
08
Chapter Eight

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.

S
Single
Responsibility
One reason to change
O
Open-Closed
Open for extension, closed for modification
L
Liskov
Substitution
Subtypes must be substitutable
I
Interface
Segregation
No forced unused dependencies
D
Dependency
Inversion
Depend on abstractions
S — Single Responsibility Principle (SRP)

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."

— Robert C. Martin

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:

DataRetriever
ReportValidator
DataProcessor
ReportFormatter
ReportSender

Each module is separate — changes only affect that module.

Monolith (Violation)

C MonolithicReportGenerator retrieveData() : Data validateData(data : Data) : boolean processData(data : Data) : ProcessedData formatReport(pd : ProcessedData) : Report sendReport(report : Report) ⚠ any change → full class modification

One class handles: retrieve, validate, process, format, and send reports. Any database or format change requires full class modification.

SRP Applied

C DataRetriever retrieveData() : Data validates C ReportValidator validateData(data) : boolean processes C DataProcessor processData(data) : ProcessedData formats C ReportFormatter formatReport(pd) : Report sends C ReportSender sendReport(report) : Status each class = one responsibility
  • 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
O — Open-Closed Principle (OCP)

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."

— Bertrand Meyer
OCP Example — Shape Area Calculator
AreaCalculator shapes: List<Shape> calculateTotalArea(): double CLOSED contains many ▼ Shape (abstract) getArea(): double OPEN Circle getArea(): double Square getArea(): double Triangle ✨ getArea(): double NEW ✓ Adding Triangle = no change to AreaCalculator or Circle/Square extend by adding new classes, not modifying existing ones
🐧

Device Drivers (OS)

Linux Kernel 🔒 CLOSED for modification Driver Interface (API) ⌨️ Keyboard NEW 🖱️ Mouse NEW 🔌 USB NEW 🌐 Network NEW ✓ new drivers extend kernel without modifying it

Linux Kernel is closed for modification. New device drivers (Keyboard, Mouse, USB, Network) extend it without changing the kernel.

🔩

IDE Plugins

IntelliJ IDEA 🔒 CLOSED for modification Plugin API 🧪 JUnit NEW 🧪 TestNG NEW 🐙 GitHub NEW 🐳 Docker NEW ✓ new plugins extend IDE without modifying core

IntelliJ IDEA is closed for modification. New plugins (JUnit, TestNG, GitHub) extend it without changing the IDE core.

L — Liskov Substitution Principle (LSP)

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."

— Barbara Liskov
LSP Example — Report Printer
ReportPrinter report: Report printReports(): void uses ▼ Report (interface) open(): void read(): String EngineeringReport ✓ open(): void read(): String BusinessReport ✓ open(): void read(): String FinancialReport ✗ open(): void read(): String setPassword(String): void ⚠ ✓ substitutable ✓ substitutable ✗ requires extra setup! ReportPrinter calls: report.open(); print(report.read()); FinancialReport breaks this — needs setPassword() first → LSP violation

LSP Satisfied

// Works for ANY Report implementation Report r = new EngineeringReport (); r. open (); print (r. read ()); // ✓ No surprises, no extra setup

EngineeringReport, BusinessReport all implement Report correctly. ReportPrinter can use any of them interchangeably.

LSP Violated — FinancialReport

// Caller MUST know the subtype! if (r instanceof FinancialReport ) { (( FinancialReport )r). setPassword (pw); } r. open (); // may throw ⚠ print (r. read ()); // ✗ tight coupling + side effects

FinancialReport is more restrictive (throws unauthorized exception) and requires additional setup (setPassword). Caller must add an instanceof check — tight coupling and side effects.

I — Interface Segregation Principle (ISP)

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
ISP Example — MediaLibrary
✗ BEFORE (Fat Interface) MediaLibrary ✗ storeBookDetails() playAudio() playVideo() BookStore storeBook() ✓ playAudio() ✗ playVideo() ✗ MusicApp storeBook() ✗ playAudio() ✓ playVideo() ✗ VideoApp storeBook() ✗ playAudio() ✗ playVideo() ✓ forced to implement unused methods! ✓ AFTER (Segregated) BookLibrary ✓ storeBookDetails() AudioLibrary ✓ playAudio() VideoLibrary ✓ playVideo() BookStore storeBook() ✓ MusicApp playAudio() ✓ VideoApp playVideo() ✓ each client implements only what it uses Split: MediaLibrary → BookLibrary + AudioLibrary + VideoLibrary Many specific interfaces > one fat interface
⚠️ Don't Exaggerate ISP! Observe the KISS principle. Group clients into categories and design for these categories. Over-segregation creates its own complexity.
D — Dependency Inversion Principle (DIP)

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.

DIP Example — FlightReservationService
HIGH-LEVEL MODULE FlightReservationService dbRepository: IDataRepository logger: ILogger makeReservation(): void depends on ▼ depends on ▼ ABSTRACTIONS IDataRepository (interface) saveReservation(): void ILogger (interface) log(String): void implements ▲ implements ▲ LOW-LEVEL MODULES (details) SqlDatabaseRepository saveReservation(): void FileLogger log(String): void High-level → abstractions ← low-level (both depend on interfaces, not each other)
Note: DIP is NOT the same as Dependency Injection (DI) or Inversion of Control (IoC) DIP is a design principle. DI and IoC are implementation patterns/techniques that can be used to achieve DIP but are separate concepts.
SOLID — Quick Reference
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
📋 SOLID Principles — Summary
  • 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
Summary — All Design Principles at a Glance
01 · Introduction

Why Design Principles?

  • Guiding rules for structuring software
  • Reduce complexity & improve maintainability
  • Apply at component, module & system level
02 · Info Hiding, SoC & Modularity

Foundational Trio

  • Information Hiding — hide internals behind stable interfaces
  • SoC — separate business logic from technology concerns
  • Modularity — encapsulated units with clear public APIs
03 · Loose Coupling

Minimize Dependencies

  • 7 types: call, message, creation, shared data, time, location, inheritance
  • Keep dependencies simple & few
  • Enables independent change & deployment
04 · High Cohesion

Related Things Together

  • Elements within a component should be strongly related
  • High cohesion → low coupling
  • Challenging to achieve & measure in practice
05 · KISS, YAGNI & DRY

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
06 · Murphy's & Postel's Law

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
07 · Abstraction & Conceptual Integrity

Generalize & Stay Consistent

  • Abstraction — hide details, depend on generalizations
  • Conceptual Integrity — consistent, recognizable concepts
  • Reduces learning curve, improves understandability
08 · SOLID Principles

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