Programming Paradigms
Twenty fundamental approaches to writing software — explained in plain English, with real vocabulary, code examples, and guidance on when each one shines.
Last reviewed:
Object-Oriented Programming (OOP)
Model software as interacting objects that combine data and behaviour.
Core Concept
Real-world entities are represented as objects — each object bundles its data (fields) and the actions it can perform (methods). You design systems by defining classes (blueprints) and creating objects from them.
Key Vocabulary
- Class
- A blueprint or template describing what an object looks like and what it can do. Like architectural plans — the plans are not a building, but you build from them.
- Object (Instance)
- A single instance created from a class. If User is the class, the user Alice (with her specific email and name) is an object.
- Inheritance
- A child class extends a parent class and inherits its properties and methods. AdminUser extends User — it has everything User has, plus admin-specific permissions.
- Polymorphism
- Different objects respond to the same method call in their own way. Both Dog and Cat have a speak() method — Dog says "Woof", Cat says "Meow".
- Encapsulation
- Hiding internal data and only exposing a clean public interface. The engine of a car is hidden — you only interact with the steering wheel and pedals.
- Abstraction
- Hiding complex implementation details and showing only the essential features. A TV remote abstracts all the electronics — you just press the button.
📋 Code example
// TypeScript OOP example
class User {
private email: string;
protected name: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email; // private — only readable via getter
}
greet(): string {
return `Hello, I'm ${this.name}`;
}
getEmail(): string { // encapsulation: controlled access
return this.email;
}
}
class AdminUser extends User { // inheritance
private permissions: string[];
constructor(name: string, email: string, permissions: string[]) {
super(name, email); // call parent constructor
this.permissions = permissions;
}
greet(): string { // polymorphism: overrides parent method
return `${super.greet()} (Admin)`;
}
}
const alice = new User('Alice', 'alice@example.com');
const bob = new AdminUser('Bob', 'bob@example.com', ['users:write']);
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.greet()); // "Hello, I'm Bob (Admin)" Functional Programming (FP)
Build programs by composing pure, stateless functions — no hidden side effects.
Core Concept
Functions are treated like values — you pass them around, compose them, and return them. The key rule: a function always returns the same output for the same input, and never modifies anything outside itself.
Key Vocabulary
- Pure Function
- A function with no side effects: given the same inputs, it always returns the same output and never modifies external state. add(2, 3) is always 5 — no matter when or how often you call it.
- Immutability
- Data is never modified after creation. Instead of changing an existing array, you return a new one. This eliminates a whole class of bugs caused by unexpected mutation.
- Higher-Order Function
- A function that takes another function as a parameter or returns one. map(), filter(), and reduce() are classic examples.
- map / filter / reduce
- map transforms every element; filter keeps only matching elements; reduce folds a list into a single value. The three fundamental FP array operations.
- Side Effects
- Anything a function does beyond returning a value: writing to a database, logging, modifying a variable. FP minimises side effects — they are isolated and explicit.
- Referential Transparency
- An expression can be replaced with its value without changing the program's behaviour. If getUser(1) always returns the same User, you can cache or reason about it freely.
📋 Code example
// Functional style in TypeScript — no mutations, no side effects
// Pure function: same input → always same output
const add = (a: number, b: number): number => a + b;
// Immutable transformation — returns NEW array, never mutates original
const prices = [10, 25, 8, 45, 15];
const discounted = prices
.filter(p => p >= 10) // keep items $10 or more
.map(p => p * 0.9) // apply 10% discount
.reduce((sum, p) => sum + p, 0); // total after discount
// 85.5 — computed without modifying 'prices'
// Higher-order function: takes a function, returns a function
const withLogging = <T, R>(fn: (arg: T) => R) =>
(arg: T): R => {
console.log(`Calling with: ${arg}`);
const result = fn(arg);
console.log(`Result: ${result}`);
return result;
};
const addTax = withLogging((price: number) => price * 1.2);
addTax(100); // logs inputs/outputs, returns 120 Declarative vs. Imperative
Declarative says "what you want". Imperative says "how to do it step by step".
Core Concept
These are two opposite styles of writing code. Imperative code describes every step of the algorithm. Declarative code describes the desired outcome and lets the runtime figure out the steps.
Key Vocabulary
- Imperative
- You write explicit instructions for the machine: "do this, then do this, then check this condition, then loop". The control flow is up to you.
- Declarative
- You describe the desired outcome: "give me all users where active = true". The system (SQL engine, React runtime) decides how to execute it.
- Abstraction Level
- Declarative code operates at a higher level of abstraction — you express intent, not mechanism. HTML says "this is a heading", not "draw pixels at these coordinates".
📋 Code example
// ── IMPERATIVE: how to do it ────────────────────────────────────
// Filter active users — you manage the loop and accumulator
const users = [
{ name: 'Alice', active: true },
{ name: 'Bob', active: false },
{ name: 'Carol', active: true },
];
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUsers.push(users[i]);
}
}
// ── DECLARATIVE: what you want ───────────────────────────────────
// Same result — you state the intent, filter handles the loop
const activeUsers2 = users.filter(u => u.active);
// ── DECLARATIVE: SQL ─────────────────────────────────────────────
// SELECT name FROM users WHERE active = true;
// You declare the result you want — the DB engine optimises the path
// ── DECLARATIVE: Terraform IaC ───────────────────────────────────
// resource "aws_s3_bucket" "my_bucket" {
// bucket = "my-app-assets"
// }
// You declare "I want this bucket to exist" — Terraform figures out how Procedural Programming
Organise code into reusable named procedures (functions) that execute step by step.
Core Concept
Procedural programming is the simplest paradigm: write a series of instructions that execute top to bottom, and group reusable logic into named procedures or functions. Older than OOP, still dominant in C, shell scripts, and many algorithms.
Key Vocabulary
- Procedure / Routine
- A named, reusable block of code that performs a task. In modern terms: a function without a return value (void) — like printReport() or saveToFile(path).
- Call Stack
- When procedure A calls procedure B which calls procedure C, they stack up. When C returns, control goes back to B, then to A. The call stack tracks this return order.
- Global State
- Variables accessible from any procedure. Procedural code often relies on shared global state, which is its biggest weakness — unexpected mutations are hard to trace.
- Modular Programming
- Splitting procedures into separate files/modules. An improvement over monolithic procedural code — functions are reusable across programs.
📋 Code example
// C-style procedural approach (TypeScript analogy)
// Each procedure does one thing
function readConfig(path: string): string {
return fs.readFileSync(path, 'utf-8');
}
function parseConfig(raw: string): Record<string, string> {
return Object.fromEntries(
raw.split('\n').map(line => line.split('='))
);
}
function startServer(config: Record<string, string>): void {
const port = parseInt(config['PORT'] ?? '3000');
console.log(`Server starting on port ${port}`);
// ...
}
// Main entry point: call procedures in order
function main(): void {
const raw = readConfig('./config.env');
const config = parseConfig(raw);
startServer(config);
}
main(); Event-Driven Programming
Code reacts to events — things that happen — rather than running top to bottom.
Core Concept
Instead of a linear flow, the program sits idle until an event occurs — a user click, an HTTP request arriving, a message appearing in a queue. Event handlers (callbacks/listeners) then respond to each event.
Key Vocabulary
- Event
- Something that happens that the program can respond to: a button click, an HTTP request, a file change, a timer firing, a message arriving in a queue.
- Event Listener / Handler
- A function registered to run when a specific event occurs. element.addEventListener('click', handleClick) registers handleClick to run on every click.
- Callback
- A function passed to another function to be called when an event completes. The classic pattern before promises and async/await.
- Event Loop
- JavaScript's mechanism for handling async events on a single thread. It continuously checks the event queue and runs callbacks when the main thread is free.
- Publish / Subscribe (Pub/Sub)
- An event pattern where publishers emit events without knowing who's listening, and subscribers react without knowing who published. Decouples components.
- Message Queue
- In backend event-driven systems (RabbitMQ, Kafka, SQS), events are persisted in a queue. Consumers process them when ready — enabling asynchronous, scalable processing.
📋 Code example
// Browser event-driven example
const button = document.getElementById('submit-btn');
// Register a listener — code runs only when the event fires
button?.addEventListener('click', (event: MouseEvent) => {
event.preventDefault();
console.log('Button clicked! Submitting form...');
submitForm();
});
// Node.js EventEmitter — backend pub/sub
import { EventEmitter } from 'events';
const orderBus = new EventEmitter();
// Subscribers — registered independently and decoupled from each other
orderBus.on('order:placed', (order) => sendConfirmationEmail(order));
orderBus.on('order:placed', (order) => reserveInventory(order));
orderBus.on('order:placed', (order) => notifyWarehouse(order));
// Publisher — emits the event, doesn't know who's listening
function placeOrder(order: Order) {
saveOrderToDatabase(order);
orderBus.emit('order:placed', order); // triggers all listeners
} Reactive Programming
Treat data as streams that change over time — and declaratively wire how changes propagate.
Core Concept
Reactive programming models data sources as observable streams. You define transformations on those streams, and any time the source emits a new value, the transformation pipeline automatically re-runs. Popularised by RxJS in JavaScript.
Key Vocabulary
- Observable
- A data source that emits values over time — like an async iterator. You subscribe to it and receive values as they arrive: HTTP responses, mouse moves, WebSocket messages.
- Stream
- A sequence of values emitted over time. A stream of click events, a stream of price updates, a stream of HTTP responses — all can be processed with the same operators.
- Subscription
- The act of "listening to" an observable. When you subscribe, you provide handlers for: next value, error, and completion. Don't forget to unsubscribe to avoid memory leaks.
- Operator
- A function that transforms a stream: map, filter, debounceTime, switchMap, mergeMap. Chain operators to build declarative data transformation pipelines.
- Backpressure
- When a producer emits values faster than the consumer can process them, backpressure mechanisms (buffering, dropping, windowing) prevent overwhelming the consumer.
- Subject
- Both an observable and an observer — it can receive values (next()) and emit them to all subscribers. Used as a bridge between non-reactive and reactive code.
📋 Code example
// RxJS example: autocomplete search with debounce
import { fromEvent, switchMap, debounceTime, distinctUntilChanged, map } from 'rxjs';
import { ajax } from 'rxjs/ajax';
const input = document.getElementById('search') as HTMLInputElement;
fromEvent(input, 'input') // Stream of keyboard events
.pipe(
map((e: Event) => (e.target as HTMLInputElement).value),
debounceTime(300), // wait 300ms after user stops typing
distinctUntilChanged(), // don't search if value didn't change
switchMap(query => // cancel previous HTTP request, start new one
ajax.getJSON(`/api/search?q=${query}`)
)
)
.subscribe({
next: results => renderResults(results),
error: err => showError(err),
});
// Without reactive: you'd manually manage timers, XHR cancellation,
// and deduplication — all with state variables and callbacks. Logic Programming
Describe facts and rules, then ask a question — the engine searches for values that satisfy them.
Core Concept
Instead of writing an algorithm, you declare facts and rules of inference. You then pose a query, and the runtime performs an automatic search (with backtracking) to find bindings that make the query true.
Key Vocabulary
- Fact
- An unconditional base assertion. parent(alice, bob). states that Alice is a parent of Bob.
- Rule
- A conditional inference: HEAD :- BODY means "HEAD holds if BODY holds". grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
- Query / Goal
- A question posed to the engine, e.g. ?- grandparent(alice, W). The engine searches for a W that satisfies the rules.
- Unification
- Matching a query against facts/rules by binding variables so both sides become identical.
- Backtracking
- When a chosen path fails to satisfy every goal, the engine automatically rewinds and tries the next alternative.
📋 Code example
% Prolog
parent(alice, bob).
parent(bob, carol).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
?- grandparent(alice, W).
% W = carol. -- the engine found it by search, not by an algorithm you wrote Aspect-Oriented Programming (AOP)
Pull cross-cutting concerns — logging, security, transactions — out of business logic into separate "aspects".
Core Concept
Some behaviour (logging, auth checks, metrics) needs to run around many unrelated functions. AOP lets you define that behaviour once, then "weave" it into chosen join points instead of duplicating calls everywhere.
Key Vocabulary
- Aspect
- A module that encapsulates a cross-cutting concern, e.g. a LoggingAspect or TransactionAspect.
- Join Point
- A point in program execution where an aspect can be applied — typically a method call.
- Pointcut
- An expression that selects which join points an aspect applies to, e.g. "every method in the service package".
- Advice
- The actual code that runs at a join point — before, after, or around it.
- Weaving
- The process (compile-time, load-time, or runtime) of merging aspects into the target code.
📋 Code example
// TypeScript decorator — a lightweight AOP-style "advice" around a method
function logCall(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: unknown[]) {
console.log(`→ calling ${key}(${JSON.stringify(args)})`);
const result = original.apply(this, args);
console.log(`← ${key} returned`, result);
return result;
};
}
class OrderService {
@logCall
placeOrder(id: string) {
return { id, status: 'placed' };
}
}
// Logging is defined once and applied without touching placeOrder's body Concurrent Programming
Structure a program as multiple tasks that can be in progress at the same time, coordinating shared resources safely.
Core Concept
Concurrency is about dealing with many things at once — tasks can start, run, and complete in overlapping time periods, whether or not they execute in true parallel. The core challenge is safely coordinating access to shared state.
Key Vocabulary
- Thread
- An independent path of execution within a process, sharing memory with other threads in the same process.
- Race Condition
- A bug where the outcome depends on the unpredictable timing/order of concurrent operations touching shared state.
- Mutex / Lock
- A mechanism ensuring only one thread accesses a critical section of shared data at a time.
- Deadlock
- Two or more threads wait forever for locks the others hold — no thread can proceed.
- Atomic Operation
- An operation that completes as a single, indivisible step — no other thread can observe it half-done.
📋 Code example
// Race condition vs. safe concurrent access (conceptual)
let counter = 0;
// UNSAFE if run from multiple threads/workers concurrently:
function incrementUnsafe() { counter = counter + 1; } // read-modify-write, not atomic
// SAFE: use a lock or an atomic primitive so the read-modify-write
// cannot be interleaved with another thread's read-modify-write
import { Mutex } from 'async-mutex';
const lock = new Mutex();
async function incrementSafe() {
const release = await lock.acquire();
try {
counter = counter + 1;
} finally {
release();
}
} Parallel Programming
Split one computation across multiple CPU cores (or machines) so it genuinely runs at the same time and finishes faster.
Core Concept
Where concurrency is about structure, parallelism is about speed: dividing a workload into independent chunks that literally execute simultaneously on multiple cores, then combining the results.
Key Vocabulary
- Data Parallelism
- The same operation applied simultaneously to many pieces of data, e.g. summing chunks of a huge array on different cores.
- Task Parallelism
- Different, independent tasks run simultaneously — e.g. resizing an image while compressing another.
- SIMD
- Single Instruction, Multiple Data — one CPU instruction operates on several values at once (vectorisation).
- Speedup
- How much faster a parallel version runs compared to the sequential one, ideally close to the number of cores used.
- Amdahl's Law
- A formula showing that the non-parallelisable portion of a program caps the maximum possible speedup, no matter how many cores you add.
📋 Code example
// Python: parallel map across CPU cores
from multiprocessing import Pool
def square(n: int) -> int:
return n * n
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(square, range(1_000_000))
# The 1M squarings are split across 4 worker processes running in parallel Dataflow Programming
Model a program as a graph of nodes connected by data flowing through them — execution follows data availability, not a fixed order.
Core Concept
A dataflow program is a directed graph: nodes are operations, edges carry data. A node "fires" as soon as its inputs are ready, so the runtime — not you — decides scheduling order.
Key Vocabulary
- Node / Operator
- A unit of computation in the graph — e.g. filter, aggregate, or a spreadsheet cell formula.
- Edge / Channel
- A connection carrying data from one node's output to another node's input.
- Firing Rule
- The condition under which a node executes — typically "all required inputs have arrived".
- Pipeline
- A linear chain of dataflow stages, each transforming the output of the previous one.
📋 Code example
# Unix pipeline — a classic dataflow graph on the command line
cat access.log | grep "ERROR" | cut -d' ' -f1 | sort | uniq -c | sort -rn
# Each stage fires as soon as data flows in from the previous one —
# grep doesn't wait for cat to finish; they run concurrently on the stream Generic Programming
Write algorithms and data structures parameterised by type, so the same code works safely across many types.
Core Concept
Instead of writing a separate sortNumbers, sortStrings, sortUsers, you write one generic sort<T> that the compiler specialises (or checks) for whatever type T you use it with.
Key Vocabulary
- Type Parameter
- A placeholder type, conventionally T, filled in when the generic is used, e.g. Array<T>, Box<User>.
- Generic Constraint
- A restriction on what T can be, e.g. "T must implement Comparable" — needed if the generic code calls methods on T.
- Type Erasure
- In some languages (Java), generic type info is removed at compile time and only checked statically — it doesn't exist at runtime.
- Monomorphization
- In other languages (Rust, C++ templates), the compiler generates a separate concrete version of the generic code per type used — faster, but bigger binaries.
📋 Code example
// TypeScript generic function — works for any type T
function firstOrDefault<T>(items: T[], fallback: T): T {
return items.length > 0 ? items[0] : fallback;
}
firstOrDefault<number>([1, 2, 3], 0); // 1
firstOrDefault<string>([], 'none'); // "none"
// Constrained generic: T must have a .length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
} Metaprogramming
Write code that generates, inspects, or modifies other code (or itself), at compile time or runtime.
Core Concept
Metaprogramming treats code as data that can be examined and manipulated programmatically — generating boilerplate, adding behaviour, or building DSLs without hand-writing every line.
Key Vocabulary
- Reflection
- Inspecting a program's own structure at runtime — listing a class's methods, or checking a value's type dynamically.
- Macro
- Code that runs at compile time to generate or transform other code before it is compiled/executed.
- Code Generation
- Producing source or bytecode programmatically, e.g. an ORM generating SQL-mapping classes from a database schema.
- Decorator / Annotation
- Syntax that attaches metadata or wraps a function/class with extra behaviour without changing its own source.
📋 Code example
# Python: a decorator is metaprogramming — it wraps the function it decorates
import functools, time
def timed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timed
def slow_query():
time.sleep(0.2)
return "done"
slow_query() # prints timing, then returns "done" — timed() never touched by caller Structured Programming
Restrict control flow to sequence, selection, and iteration — no unconstrained jumps — so programs stay easy to reason about.
Core Concept
Structured programming was a deliberate reaction against "spaghetti code" built from GOTO statements. It insists that every block have one entry and one exit, composed only from sequence (do this, then this), selection (if/else), and iteration (loops).
Key Vocabulary
- Sequence
- Statements executed one after another, top to bottom.
- Selection
- Branching logic — if/else, switch — choosing which block runs based on a condition.
- Iteration
- Repeating a block while a condition holds — for, while, do-while loops.
- Single Entry / Single Exit
- Each control block has exactly one way in and one way out, making the flow easy to trace top to bottom.
- GOTO
- An unconditional jump to an arbitrary label — the construct structured programming was designed to eliminate.
📋 Code example
// Structured: sequence, selection, iteration — no jumps to arbitrary labels
function classify(n: number): string {
let result = ''; // sequence
if (n < 0) { // selection
result = 'negative';
} else {
for (let i = 2; i <= n; i++) { // iteration
if (n % i === 0 && i !== n) {
result = 'composite';
break; // structured exit, not a GOTO
}
}
if (!result) result = 'prime or one';
}
return result;
} Actor Model
Model concurrency as independent "actors" that only communicate by sending asynchronous messages — no shared mutable state.
Core Concept
Each actor is an isolated unit with its own private state and a mailbox. Actors never call each other's methods or touch shared memory — they only send messages, which sidesteps most race-condition and locking problems by design.
Key Vocabulary
- Actor
- An independent unit of computation with private state, a mailbox, and behaviour describing how it reacts to messages.
- Mailbox
- A queue of incoming messages an actor processes one at a time, so its own state never needs locks.
- Message Passing
- Actors communicate exclusively by sending immutable messages, never by direct method calls or shared memory.
- Supervision
- A parent actor monitors its children and decides how to recover them (restart, stop, escalate) when they crash.
- Location Transparency
- Sending a message to an actor works the same whether it's local or on another machine — the model hides the network.
📋 Code example
# Elixir: two actors (processes) communicating only via messages
defmodule Counter do
def start, do: spawn(fn -> loop(0) end)
defp loop(count) do
receive do
{:increment, from} ->
send(from, {:count, count + 1})
loop(count + 1)
end
end
end
counter = Counter.start()
send(counter, {:increment, self()})
receive do
{:count, value} -> IO.puts("Count is now #{value}")
end
# No shared memory, no locks — just messages between isolated processes Prototype-Based Programming
Objects inherit directly from other objects (prototypes) instead of from classes.
Core Concept
There is no separate 'blueprint' concept — every object can serve as a prototype for new objects. A new object either copies its prototype's properties or delegates lookups to it when a property is missing.
Key Vocabulary
- Prototype
- An existing object that a new object is based on, either by cloning or by delegation.
- Prototype Chain
- The link from an object to its prototype, to that prototype's prototype, and so on — used to resolve property lookups.
- Delegation
- When an object is asked for a property it doesn't have, it forwards ("delegates") the lookup to its prototype.
- Clone
- Creating a new object by copying an existing one, rather than instantiating from a class definition.
📋 Code example
// JavaScript: prototype-based inheritance without "class"
const animal = {
speak() { return `${this.name} makes a sound.`; }
};
const dog = Object.create(animal); // dog's prototype is 'animal'
dog.name = 'Rex';
console.log(dog.speak());
// "Rex makes a sound." — speak() was found by walking the prototype chain,
// not because dog has its own copy of the method Constraint Programming
Declare constraints a solution must satisfy, and let a solver search the space of possibilities for you.
Core Concept
You don't write a search algorithm — you describe variables, their domains, and the constraints between them (equations, inequalities, logical relations). A constraint solver explores the space and returns assignments that satisfy everything, or proves none exist.
Key Vocabulary
- Constraint
- A restriction a solution must satisfy, e.g. "x + y = 10" or "no two employees share a shift".
- Domain
- The set of possible values a variable can take, e.g. an integer between 1 and 100.
- Solver
- The engine that searches for an assignment satisfying all constraints, often using backtracking plus pruning.
- Propagation
- Eliminating impossible values from domains early, based on already-known constraints, to shrink the search space.
- Satisfiability
- Whether a set of constraints has at least one valid solution at all.
📋 Code example
# Python with a constraint solver (OR-Tools CP-SAT, simplified)
from ortools.sat.python import cp_model
model = cp_model.CpModel()
x = model.NewIntVar(0, 10, 'x')
y = model.NewIntVar(0, 10, 'y')
model.Add(x + y == 10)
model.Add(x < y)
solver = cp_model.CpSolver()
if solver.Solve(model) == cp_model.OPTIMAL:
print(solver.Value(x), solver.Value(y)) # e.g. 4 6
# You declared the constraints; the solver found values satisfying both Symbolic Programming
Manipulate symbols and expressions themselves — code as data — rather than only numeric or string values.
Core Concept
Symbolic programs treat code (and mathematical expressions) as first-class data structures that can be built, inspected, transformed, and evaluated at will — blurring the line between program and data.
Key Vocabulary
- Symbol
- An identifier treated as a value in its own right, not evaluated — e.g. 'foo in Lisp is the symbol foo, not a call to it.
- S-Expression
- A nested-list notation, e.g. (+ 1 2), that represents both code and data uniformly in Lisp-family languages.
- Homoiconicity
- A language property where code is written in the language's own basic data structure — making code trivially inspectable and generable as data.
- Quote / Eval
- quote prevents an expression from being evaluated (treat it as data); eval takes data and runs it as code.
📋 Code example
; Scheme/Lisp: code and data share the same list structure
(define expr '(+ 1 (* 2 3))) ; quote — treat this as DATA, don't evaluate it
(display expr) ; prints: (+ 1 (* 2 3))
(eval expr) ; now evaluate that data AS CODE
; => 7
; This is what lets Lisp macros write programs that write programs Literate Programming
Interleave source code with natural-language explanation in one document, then extract runnable code and formatted docs from it.
Core Concept
Rather than writing code first and comments as an afterthought, you write a narrative document aimed at a human reader, with code fragments embedded in it. Tools then 'tangle' the code into a runnable program and 'weave' the prose into readable documentation.
Key Vocabulary
- Tangle
- The process of extracting the actual source code from a literate document, reassembling fragments in the right order.
- Weave
- The process of producing formatted, readable documentation (PDF/HTML) from the same literate document.
- Chunk
- A named, embeddable block of code within the narrative, referenced and combined to build the full program.
- Noweb
- A simple, language-agnostic literate-programming tool/format for defining chunks and weaving/tangling them.
📋 Code example
<<define-average>>=
def average(numbers):
"""Computes the arithmetic mean of a non-empty list."""
return sum(numbers) / len(numbers)
@
The function above is the core of our report generator. It is used
in the summary step below, where we combine it with the parsed input:
<<use-average>>=
scores = parse_scores(raw_input)
print(f"Average score: {average(scores)}")
@
<!-- "tangle" extracts define-average + use-average into a .py file;
"weave" turns this whole document into readable docs --> Data-Oriented Programming
Design around plain, immutable data and separate functions from data, optimising for data layout and transformations over object hierarchies.
Core Concept
Instead of bundling data and behaviour into objects, you keep data as plain, generic structures (maps, arrays, records) and write functions that transform that data. This favours simplicity, serialisability, and — in performance-critical contexts — cache-friendly memory layout.
Key Vocabulary
- Data as Data
- Data is represented with generic, transparent structures (maps, vectors) rather than custom classes with hidden behaviour.
- Structure of Arrays
- Storing each field of a collection of records in its own contiguous array, instead of an array of objects — better for cache locality and SIMD.
- Immutability
- Data structures are not mutated in place; transformations return new versions, making state changes explicit and traceable.
- Decoupled Functions
- Behaviour lives in standalone functions that take data in and return data out, not as methods attached to that data.
📋 Code example
// Data-oriented: plain data + separate transform functions (TypeScript)
type Player = { id: string; hp: number; x: number; y: number };
function damage(player: Player, amount: number): Player {
return { ...player, hp: Math.max(0, player.hp - amount) }; // new data, not mutated
}
function move(player: Player, dx: number, dy: number): Player {
return { ...player, x: player.x + dx, y: player.y + dy };
}
// No Player class with methods — just data and functions that transform it
let p: Player = { id: 'p1', hp: 100, x: 0, y: 0 };
p = move(damage(p, 20), 5, 0); Frequently Asked Questions
What's the difference between concurrency and parallelism?
Concurrency is about structure — tasks can start, run, and complete in overlapping time periods without necessarily executing at the exact same instant, which is what lets a single-threaded event loop juggle many in-flight operations. Parallelism is about speed — a workload is literally split across multiple CPU cores or machines so parts of it execute simultaneously. A program can be concurrent without being parallel (Node.js's single-threaded event loop), and code can be parallel without dealing with concurrency's coordination problems if the tasks are fully independent.
What's the difference between imperative and declarative code?
Imperative code spells out every step of how to reach a result — explicit loops, conditionals, and mutable accumulators the programmer controls directly. Declarative code states the desired outcome and lets the runtime figure out the steps, like a SQL SELECT statement or a React JSX tree. The trade-off is that declarative code is usually more readable, but you give up fine-grained control over exactly how the runtime executes it, which can make performance harder to reason about.
What's the difference between OOP inheritance and prototype-based delegation?
Classical OOP inheritance (Java, C#) defines a class hierarchy at compile/design time — a child class extends a parent class and inherits its structure. Prototype-based languages like JavaScript have no separate 'class' concept underneath — every object can serve as a prototype for another, and property lookups that miss on an object delegate up the prototype chain to find them. JavaScript's class keyword is really just syntax sugar over this same underlying prototype chain.
What makes a function 'pure' in functional programming, and why does it matter?
A pure function has no side effects: given the same inputs it always returns the same output, and it never modifies anything outside itself (no writing to a database, no mutating external variables). This property is what enables referential transparency — a pure function call can be replaced with its result, cached, tested in isolation, or safely run in parallel, none of which is safe to assume about a function with hidden side effects.
What's the difference between a race condition and a deadlock?
A race condition happens when the outcome of concurrent operations depends on unpredictable timing — two threads read-modify-write shared state and one overwrites the other's update. A deadlock happens when two or more threads each hold a lock the other is waiting for, so neither can ever proceed and the program simply hangs. A race condition corrupts data silently; a deadlock freezes execution entirely.
How does the Actor Model avoid the locking problems of shared-memory concurrency?
In the Actor Model, each actor owns private state that no other actor can touch directly, and actors communicate exclusively by sending asynchronous messages into each other's mailboxes, processed one at a time. Because there's no shared mutable memory between actors, there's nothing to lock — most classic race conditions are sidestepped by design, though the model introduces its own failure modes like mailbox overload and needing explicit supervision strategies.
What's the difference between generic type erasure and monomorphization?
Type erasure (used by Java) strips generic type information after compile-time checking, so a List<String> and a List<Integer> are represented identically at runtime with no type distinction preserved. Monomorphization (used by Rust and C++ templates) does the opposite — the compiler generates a separate concrete version of the generic code for each type it's actually used with, which is faster at runtime but produces larger compiled binaries.
What does 'homoiconicity' mean and why does it matter for metaprogramming?
A homoiconic language represents its own code using the same data structure it uses for ordinary data — in Lisp/Scheme, code is written as nested lists (S-expressions), the exact same structure used for lists of data. That property is what makes Lisp-style macros so powerful: because code is just data, a program can construct, inspect, and transform other code using the language's normal list-manipulation functions, rather than needing a separate templating layer.
Why is Amdahl's Law relevant when deciding whether to parallelize code?
Amdahl's Law states that the portion of a program that can't be parallelized puts a hard ceiling on the maximum possible speedup, no matter how many cores you throw at the parallelizable portion. If only 80% of a task can run in parallel, you'll never get more than a 5x speedup even with infinite cores — which is why profiling to find the actual parallelizable hot path matters more than blindly adding concurrency.
Is this programming paradigms glossary free to use?
Yes — every paradigm, vocabulary term, and code example on this page is free to read and reference without an account or paywall.