ARTICLE · Playbook

SOLID and Friends (JS flavored)

S — Single Responsibility Principle. A module has one reason to change. Smell: UserService validates input, hashes passwords, writes to Postgres, and sends the welcome email.

ArticlePlaybook© Arthur Bernier Jr492 words6 code blocks

SOLID#

S — Single Responsibility Principle. A module has one reason to change. Smell: UserService validates input, hashes passwords, writes to Postgres, and sends the welcome email.

JS
// fix: split by reason-to-changeclass UserRepository { save(user) { /* SQL */ } }class PasswordHasher { hash(pw) { return argon2.hash(pw); } }class WelcomeMailer  { send(user) { /* SES */ } }class RegisterUser   { constructor(repo, hasher, mailer) {...} async run(input) {...} }

O — Open/Closed Principle. Open for extension, closed for modification: add behavior without editing existing code. Smell: if (type === 'card') ... else if (type === 'paypal') ... else if (type === 'crypto') growing in one function.

JS
// fix: strategy map / polymorphismconst processors = { card: new CardProcessor(), paypal: new PaypalProcessor() };function pay(order) { return processors[order.method].charge(order); } // add crypto by adding an entry

L — Liskov Substitution Principle. Subtypes must be usable wherever the base type is, without surprises. Smell: class ReadOnlyRepo extends Repo { save() { throw new Error('not supported'); } }.

JS
// fix: don't inherit capabilities you can't honor; split the interfaceclass Reader { find(id) {} }class Repo extends Reader { save(x) {} }   // ReadOnlyRepo is just a Reader

I — Interface Segregation Principle. Many small client-specific interfaces beat one fat one. Smell: every plugin must implement onInstall, onUpgrade, onRender, onSchedule, onWebhook even if it only renders.

JS
// fix: optional capability objects / duck-typed small contractsconst plugin = { render: (ctx) => {...} };             // only what it needsif (plugin.onSchedule) scheduler.register(plugin);     // check capability, not class

D — Dependency Inversion Principle. High-level policy depends on abstractions; details (DB, HTTP, SDKs) plug in. Smell: const db = require('pg'); class OrderService { place() { db.query(...) } } — business logic welded to Postgres.

JS
// fix: inject the port, implement the adapterclass OrderService { constructor({ orders, clock }) { this.orders = orders; this.clock = clock; } }new OrderService({ orders: new PgOrderRepo(pool), clock: Date });     // prodnew OrderService({ orders: new InMemoryOrderRepo(), clock: fakeClock }); // test

The rest of the vocabulary#

  • DRY (Don't Repeat Yourself) — one source of truth per piece of knowledge. Smell: the same validation regex in three files. Fix: one validators.js. Caveat: duplicated code that represents different knowledge should stay separate; premature DRY couples unrelated things.
  • KISS (Keep It Simple) — simplest thing that meets the requirement. Smell: event bus + plugin system for a CLI with two commands.
  • YAGNI (You Aren't Gonna Need It) — don't build for hypothetical requirements. Smell: storageAdapter abstraction with one implementation and no second in sight.
  • Separation of concerns — layer by kind of work: transport (HTTP handlers) / application (use cases) / domain (rules) / infrastructure (DB, queues). Smell: SQL inside an Express route handler.
  • Composition over inheritance — build behavior by combining objects/functions, not deep class trees. Smell: AdminUserWithAuditLogging extends UserWithAuditLogging extends User.
JS
const withAudit = (repo) => ({ ...repo, save: async (x) => { await repo.save(x); audit('save', x); } });const repo = withAudit(withCache(new PgRepo()));
  • Law of Demeter (principle of least knowledge) — talk to your friends, not strangers: one dot where possible. Smell: order.customer.address.country.code across modules. Fix: order.shippingCountryCode().
  • Dependency injection — the mechanism for DIP: pass collaborators in (constructor args, factory functions, a container) instead of new/require-ing them inside. Enables testing with fakes and swapping adapters.
  • Encapsulation / information hiding — expose intent, hide representation; #private fields, module scope.
  • Cohesion high, coupling low — the one-line summary of all of the above.
  • Tell, don't ask — call account.withdraw(50) not if (account.balance >= 50) account.balance -= 50.
  • Fail fast / explicit over implicit — validate at boundaries, throw early, no magic defaults.

In the interview (one breath each)#

  • "I'd keep domain logic free of frameworks and inject the DB and clock so it's testable: that's DIP via DI."
  • "Each service has one reason to change: SRP at the service level mirrors bounded contexts."
  • "Adding a new channel shouldn't touch existing code: strategy per channel, open/closed."
  • "I avoid inheritance across modules; I compose behaviors (caching, auditing) as wrappers."

See also