Episode 9 — System Design / 9.2 — Design Principles

9.2 — Design Principles: Quick Revision

Compact cheat sheet. Print-friendly.

How to use this material:

  1. Scan before an interview or exam.
  2. Use as a quick reference during code reviews.
  3. If anything is unclear, refer to the full sub-topic file.
  4. Covers ALL key concepts from 9.2.a through 9.2.d.

9.2.a — SOLID Principles

PrincipleFull NameOne-LinerViolation Smell
SSingle ResponsibilityOne class = one reason to changeGod classes, "and" in class descriptions
OOpen/ClosedExtend, don't modifyGrowing switch/if-else on types
LLiskov SubstitutionSubtypes must honor parent contractsthrow new Error('Not supported') in overrides
IInterface SegregationSmall, focused interfacesEmpty/no-op method implementations
DDependency InversionDepend on abstractions, not concretesnew ConcreteClass() inside business logic

SRP Quick Check

Ask: "Who would request changes to this class?"
Multiple stakeholders → Split the class.

OCP Pattern

// BAD:  switch(type) { case 'a': ... case 'b': ... }
// GOOD: strategies.get(type).execute()

LSP Rules

  • Preconditions: can't be strengthened in subtypes
  • Postconditions: can't be weakened in subtypes
  • Invariants: must be preserved in subtypes
  • No surprise exceptions in subtypes

DIP Pattern

High-level → Interface ← Low-level
(not: High-level → Low-level)

SOLID in Express

LayerResponsibilityPrinciple
MiddlewareCross-cutting (auth, logging, CORS)OCP
ControllerHTTP parsing + response formattingSRP
ServiceBusiness logic + orchestrationSRP + DIP
RepositoryData accessSRP + DIP

9.2.b — DRY and Other Principles

DRY (Don't Repeat Yourself)

DRY = same KNOWLEDGE in one place (not same CODE)

Identical code, different purposes → NOT a violation (accidental duplication)
Different code, same business rule → IS a violation (true duplication)

Rule of Three: abstract on the 3rd occurrence, not the 2nd

KISS (Keep It Simple, Stupid)

Default to the simplest solution that works.
Cleverness is a liability in team codebases.
Use existing libraries before building your own.

YAGNI (You Ain't Gonna Need It)

Build for today's requirements, not tomorrow's guesses.
Applies to: features, abstractions, hypothetical scenarios
Does NOT apply to: tests, error handling, security

Composition over Inheritance

Use InheritanceUse Composition
Genuine is-a relationshipHas-a or uses-a relationship
Shallow hierarchy (2-3 levels)Need to combine behaviors
Extending framework classesBuilding domain models
// Composition pattern:
class Notifier {
  private channels: NotificationChannel[] = [];
  addChannel(c: NotificationChannel) { this.channels.push(c); }
  async notifyAll(msg: string) { 
    await Promise.all(this.channels.map(c => c.send(msg))); 
  }
}

Law of Demeter

Only talk to:
  1. this (own methods)
  2. Parameters
  3. Objects you create
  4. Your direct properties

BAD:  order.getCustomer().getAddress().getCity()
GOOD: order.getDeliveryCity()

Exceptions: data objects, fluent APIs, functional chains

Separation of Concerns

Frontend: View | State | Routing
API:      Routes | Middleware | Validation
Backend:  Services | Repositories | External APIs

Over-Engineering vs Under-Engineering

FactorLean SimpleLean Architecture
Team size1-2 devs5+ devs
LifespanScript / prototypeMulti-year product
Change frequencyRarelyWeekly
Domain complexityCRUDComplex rules

9.2.c — Design Patterns

Three Categories

Creational  = How do I MAKE things?     (Factory, Singleton, Builder)
Structural  = How do I CONNECT things?  (Adapter, Decorator, Facade, Proxy)
Behavioral  = How do things TALK?       (Observer, Strategy, Command, Chain)

Most-Used Patterns in JS/TS

PatternJS/TS MechanismExample
ObserverEventEmitter, DOM eventsemitter.on('event', handler)
StrategyClosures, function objectsarray.sort(comparator)
SingletonModule cacheexport const config = {...}
Iteratorfor...of, generatorsSymbol.iterator
DecoratorHOFs, TS decoratorswithAuth(handler)
FactoryFunctions returning objectscreateConnection(type)
ProxyNative Proxy objectnew Proxy(target, handler)
Chain of Resp.Express middlewareapp.use(a).use(b).use(c)

Pattern Template

Name → Problem → Context → Solution → Consequences → Related Patterns

When to Use / Skip

Use when:  Real, recurring problem + pattern fits + complexity justified
Skip when: First/second occurrence + simple solution works + team won't recognize it

Rule of Three: 1st = write simple, 2nd = note it, 3rd = apply pattern

Common Anti-Patterns

Anti-PatternWhat It IsFix
God ObjectOne class does everythingSRP: split into focused classes
Spaghetti CodeNo structure, tangled logicSoC: modularize
Golden HammerOne tool for everythingRight tool for the job
Lava FlowDead code nobody removesDelete it; use git
Premature OptimizationOptimize before measuringProfile first

9.2.d — Writing Extensible Code

Extension Spectrum

Hardcoded → Configurable → Parameterized → Pluggable → Fully Extensible
   ↑ Scripts                     ↑ Most apps             ↑ Frameworks

Plugin Architecture

interface Plugin {
  name: string;
  version: string;
  init(app: Application): void;
  destroy?(): void;
}

// Register plugins — core never changes
pluginManager.register(authPlugin);
pluginManager.register(loggingPlugin);

Strategy Pattern

interface Strategy { execute(data: any): Result; }
class Engine {
  private strategies = new Map<string, Strategy>();
  register(name: string, s: Strategy) { this.strategies.set(name, s); }
  run(name: string, data: any) { return this.strategies.get(name)!.execute(data); }
}
// New behavior = new strategy class. Zero changes to Engine.

Feature Flags

Types: Release (days) | Experiment (weeks) | Ops (permanent) | Permission (permanent)

Capabilities: Global toggle | % rollout | User targeting | Group targeting

Best practices: Default OFF | Log evaluations | Clean up after rollout

Dependency Injection

TypeWhen to Use
ConstructorRequired dependencies (most common)
SetterOptional or changeable dependencies
InterfaceFramework-managed injection
// Composition root — one place where all deps are wired
const db = new PostgresDatabase(url);
const repo = new UserRepository(db);
const service = new UserService(repo, emailer);
const controller = new UserController(service);

// Test: swap real → mock
const mockDb = new InMemoryDatabase();
const testRepo = new UserRepository(mockDb);
const testService = new UserService(testRepo, mockEmailer);

Configuration over Code

Code (hardcoded) → Env vars → Config files → Database → Admin panel
Less flexible ──────────────────────────────────── More dynamic

Event-Driven Extensibility

// Producer doesn't know about consumers
events.emit('order:placed', order);

// Consumers subscribe independently
events.on('order:placed', sendConfirmationEmail);
events.on('order:placed', updateInventory);
events.on('order:placed', trackAnalytics);
// New consumer = new listener. Zero changes to producer.

Master Checklist — Code Review Questions

  1. Does each class/function have a single, clear responsibility?
  2. Can I add new behavior without modifying existing code?
  3. Are subtypes safe substitutes for their parents?
  4. Are interfaces small and focused?
  5. Do high-level modules depend on abstractions?
  6. Is knowledge represented in exactly one place?
  7. Is this the simplest solution that works?
  8. Am I building something I actually need right now?
  9. Am I using composition instead of deep inheritance?
  10. Can I test this in isolation?

Return to Overview