SOLID is five principles of object-oriented design: single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion. Robert C. Martin gathered them. His own summary on The Principles of OOD is that they “focus very tightly on dependency management”. Each one is about what a piece of code depends on, and what has to change when something it depends on changes.

Each principle below has a plain statement, the position the Prickles canon takes on it and a short example in TypeScript, JavaScript, Python, Java and PHP. The canon here means the 51 tenets this site publishes. It keeps four of the five SOLID principles as tenets. Open-closed has no tenet of its own, because F4 Simplicity covers it. That tenet says not to add the extension point until the second caller arrives.

SOLID design principles at a glance

Letter Principle In one line In the canon
S Single responsibility Each unit has one reason to change F1 Single Responsibility Principle
O Open-closed Add behaviour by adding code, and leave working code alone Part of F4 Simplicity
L Liskov substitution A subtype works wherever its parent type is promised A9 Liskov Substitution Principle
I Interface segregation No code depends on methods it doesn’t use A10 Interface Segregation Principle
D Dependency inversion Business logic and the detail it uses both depend on an abstraction A11 Dependency Inversion Principle

Where SOLID came from

On his Principles of OOD page, Martin traces the “first glimmer” of the principles to an article he posted to the comp.object newsgroup in March 1995. That page lists all five as principles of class design, with single responsibility first. For the detail, it points to his book Agile Software Development, Principles, Patterns, and Practices (2002).

Two of the five began with other people. Martin’s paper Design Principles and Design Patterns (2000) credits open-closed to Bertrand Meyer. Wikipedia’s entry for the open-closed principle places the term in Meyer’s book Object-Oriented Software Construction (1988). The same paper credits Liskov substitution to Barbara Liskov. Wikipedia’s article on the Liskov substitution principle dates it to her 1987 conference keynote, Data Abstraction and Hierarchy.

Last came the acronym, usually credited to Michael Feathers. Wikipedia cites Martin’s Clean Architecture for that. Martin told the story himself in a 2018 InfoQ podcast:

I think it was 2004 and Michael wrote me an e-mail, and said that if you rearranged the principles they spell SOLID.

— Robert C. Martin, Uncle Bob Martin on Clean Software, Craftsperson, Origins of SOLID, DDD, & Software Ethics, 2018

Examples in five languages

All the examples use one small shop. An order there has a reference, the customer’s email address and its lines. Each line has a unit price in pence and a quantity, and prices stay in whole pence until a receipt prints them. The later examples reuse the Order type and the two functions from the first.

Each example shows code that follows the principle. The text around it describes a version that breaks it. Every example is written to the limits the Prickles lint packs set, where cyclomatic complexity counts the separate paths through a function:

  • cyclomatic complexity of 3 or less, as S2 Cyclomatic Caps sets
  • at most 15 lines and 3 parameters a function
  • no comments
  • braces on every if
  • a name for every number other than -1, 0 and 1

Single responsibility principle

A unit of code should have one reason to change. In Solid Relevance (2020), Martin puts it this way: “Gather together the things that change for the same reasons. Separate things that change for different reasons.”

A checkout function that adds up an order and also writes the receipt has two reasons to change. A new discount rule changes the total, and new wording changes the receipt. Split into two functions, each has one:

Viewing: TypeScript.
const PENCE_PER_POUND = 100;
const poundFormat = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" });

export interface OrderLine {
  readonly unitPence: number;
  readonly quantity: number;
}

export interface Order {
  readonly reference: string;
  readonly customerEmail: string;
  readonly lines: readonly OrderLine[];
}

export const orderTotalPence = (order: Order): number =>
  order.lines.reduce((total, line) => total + line.unitPence * line.quantity, 0);

export const receiptText = (totalPence: number): string =>
  `Thank you for your order. Total: ${poundFormat.format(totalPence / PENCE_PER_POUND)}`;

Prickles treats F1 Single Responsibility Principle as the keystone of the canon. Its working test asks who pays when the unit changes. The tenet also warns against the literal reading, because one reason to change doesn’t mean the smallest function you can write. The line and complexity limits are there to flag a tangle early, and deciding what the one job is stays with whoever writes and reviews the code.

Open-closed principle

Code should be open for extension and closed for modification. You add behaviour by adding code, without editing code that already works. In his 2000 paper, Martin ranks it above the others:

Of all the principles of object oriented design, this is the most important.

— Robert C. Martin, Design Principles and Design Patterns, 2000

Prickles takes a different view on timing, so open-closed sits inside F4 Simplicity and has no tenet of its own. F4’s rule is to build for the requirement at hand and leave out the parameter, flag, abstraction or option until a second caller needs it. An extension point added early is code written for a caller that doesn’t exist yet.

Suppose the shop starts with one delivery price, held in a single named constant, with nothing to extend. A delivery-pricing interface with one implementation would follow open-closed to the letter, with only the one price behind it. When next-day delivery arrives, it brings the need for an extension point with it. The smallest one that serves both methods is a table:

Viewing: TypeScript.
const DELIVERY_PENCE = { standard: 395, nextDay: 695 } as const;

export type DeliveryMethod = keyof typeof DELIVERY_PENCE;

export const deliveryPence = (method: DeliveryMethod): number => DELIVERY_PENCE[method];

A third method now means adding to the table. The code that reads the table doesn’t change. That’s still open-closed, with the extension point added when a second caller needed it. Martin Fowler’s Yagni (2015) makes the same case for waiting. YAGNI stands for “You Aren’t Gonna Need It”, and Fowler warns that an extensibility point that’s never used is “likely to also get in your way as well”. He’s also clear that YAGNI doesn’t apply to work that makes code easier to change, so keeping each unit small and well named still counts.

Liskov substitution principle

A subtype must work wherever its parent type is promised, so code written against the parent can’t tell which subtype it has. In practice, a subtype accepts every input the parent accepts, guarantees every result the parent guarantees and throws nothing the parent didn’t.

Couriers give the shop delivery quotes. In this shop, letter post takes nothing over 100 g. If its quote method threw an error above that weight, every caller written against Courier would break on the first heavy parcel. So the contract itself says a courier may decline, and letter post declines by returning no quote:

Viewing: TypeScript.
const LETTER_LIMIT_GRAMS = 100;
const LETTER_PENCE = 85;

export interface Courier {
  quotePence(weightGrams: number): number | undefined;
}

export class LetterPost implements Courier {
  quotePence(weightGrams: number): number | undefined {
    return weightGrams > LETTER_LIMIT_GRAMS ? undefined : LETTER_PENCE;
  }
}

export const quotesPence = (couriers: readonly Courier[], weightGrams: number): number[] =>
  couriers.flatMap((courier) => courier.quotePence(weightGrams) ?? []);

quotesPence handles a missing quote because the contract says one can be missing. It never checks which courier it has. A9 Liskov Substitution Principle reads every override against its parent’s inputs, results and invariants, meaning the conditions the parent promises always hold. Its answer to “prefer composition over inheritance” is that Liskov covers the times you do inherit, so the two rules don’t conflict.

Interface segregation principle

No code should depend on methods it doesn’t use, so the code that calls an interface decides its shape.

Two callers use the shop’s order store, each for its own job. The dispatch email only looks orders up, and checkout only saves them. A single OrderStore interface with both methods would tie the dispatch email to save, so a change to saving could reach code that never saves. Splitting it into two narrow interfaces keeps them apart. The database adapter implements both:

Viewing: TypeScript.
export interface OrderReader {
  find(reference: string): Promise<Order | undefined>;
}

export interface OrderWriter {
  save(order: Order): Promise<void>;
}

export const dispatchSubject = async (
  orders: OrderReader,
  reference: string
): Promise<string | undefined> => {
  const order = await orders.find(reference);
  return order ? `Order ${order.reference} is on its way` : undefined;
};

JavaScript has no interface keyword. There, the methods a function calls make up its interface, and dispatchSubject calls only find. In every language, a test of the dispatch email needs a fake with one method.

A10 Interface Segregation Principle applies the split when callers have separate needs, and accepts that more interfaces means more names to keep. Where the callers need the same methods, one interface is fine.

Dependency inversion principle

Business logic shouldn’t depend on low-level detail. Both should depend on an abstraction that the business logic owns. The 2000 paper gives the short form: “Depend upon Abstractions. Do not depend upon concretions.”

Sending a receipt after an order is shop policy. Which email service sends it is a detail that changes when the shop changes provider. So checkout declares the interface it needs. An adapter for the email service, at the edge of the system, implements it:

Viewing: TypeScript.
export interface ReceiptSender {
  send(email: string, text: string): Promise<void>;
}

export const sendReceipt = async (order: Order, receipts: ReceiptSender): Promise<void> => {
  await receipts.send(order.customerEmail, receiptText(orderTotalPence(order)));
};
Before Checkout sendReceipt uses Email library volatile detail After Checkout sendReceipt uses ReceiptSender owned by checkout both implement Email adapter at the edge Test fake records each send
Before, checkout imports the email library. After, the email code depends on an interface that checkout owns. A test fake implements the same interface.

Before, checkout depended on the email code. Now the email code depends on an interface checkout owns. That reversal gives dependency inversion its name. A test passes a fake that records what it was asked to send, so it needs no email service.

A11 Dependency Inversion Principle applies the rule to volatile detail such as the database, the email provider or a third-party API. It leaves stable utilities, time and console output alone unless a test needs a fake.

When SOLID goes too far

Each principle adds structure to the code, and where nothing needs that structure, it’s more code to read and change for no benefit. PT3 Every Pattern Has a Failure Mode asks you to name a pattern’s cost before you use it. The same habit works for SOLID:

  • Single responsibility, read as a call to make every function as small as possible, gives functions too small to mean anything alone. A reader then has to follow the calls to see the job.
  • Open-closed, applied before a second caller exists, gives extension points nobody extends.
  • Interface segregation, applied where callers need the same methods, gives more names and no benefit.
  • Dependency inversion, applied to stable code, gives interfaces with one implementation and no test fake. F4 rejects an abstraction layer with only one implementation.

Liskov substitution has no entry in that list, because it adds no structure. It only constrains a subtype you’ve already chosen to write. Sandi Metz’s The Wrong Abstraction (2016) gives the general case: “duplication is far cheaper than the wrong abstraction”.

The Prickles packs give a coding agent these tenets as rules, beside lint configs with the same limits these examples are written to. The implementation page shows what each agent gets.

How to check code against SOLID

  1. Say what each function or class does in one sentence. If the sentence needs “and”, the unit probably has two reasons to change.
  2. For each new interface, parameter or flag, name the callers that need it today. Until there’s a second one, leave it out.
  3. For each subtype, check that it accepts every input its parent accepts, returns what its parent promises and throws nothing new.
  4. For each parameter typed as an interface, list the methods the function calls. If the interface has more, split it or narrow the type.
  5. For each import in business logic, ask whether it’s a database, an email service or another detail that changes. If it is, depend on an interface the business logic owns.