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.

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.

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.
Java, C#, Python (mixed), TypeScript, Ruby, Swift, Kotlin
📋 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)"
Watch out: Deep inheritance hierarchies become fragile — changing a base class breaks all subclasses. Prefer "composition over inheritance": build objects from smaller, reusable parts rather than deep class trees.

Functional Programming (FP)

Build programs by composing pure, stateless functions — no hidden side effects.

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.

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.
Haskell, Erlang, Clojure (pure FP); JavaScript/TypeScript, Python, Scala, Rust, Swift (mixed/FP features)
📋 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
Watch out: Pure FP can be verbose for I/O-heavy tasks. Real-world code needs side effects (database writes, HTTP calls) — the FP approach is to isolate them at the edges of your system, keeping the core pure.

Declarative vs. Imperative

Declarative says "what you want". Imperative says "how to do it step by step".

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.

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".
Declarative: SQL, HTML, CSS, React JSX, Terraform, GraphQL, regex. Imperative: C, assembly, traditional loops in any language.
📋 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
Watch out: Declarative code is readable but you lose control of performance. A complex SQL query or deeply nested React tree can be slow in ways that are hard to debug without understanding what the runtime is doing underneath.

Procedural Programming

Organise code into reusable named procedures (functions) that execute step by step.

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.

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.
C, Pascal, early BASIC, shell scripts (bash), assembly. Also used within OOP/FP languages for simple scripts and utilities.
📋 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();
Watch out: Procedural code with shared global state becomes a maintenance nightmare at scale — hard to test, debug, and reason about. This is why OOP (encapsulating state in objects) and FP (avoiding shared state entirely) emerged.

Event-Driven Programming

Code reacts to events — things that happen — rather than running top to bottom.

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.

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.
Browser JS (click/input/resize events), Node.js (EventEmitter), React (onClick, onChange), message queues (Kafka, RabbitMQ, SQS), GUI frameworks, game engines.
📋 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
}
Watch out: Event-driven code can become "callback hell" or hard to trace — events fan out across many handlers and the execution order isn't obvious. Use structured logging with correlation IDs to trace an event's path through many handlers.

Reactive Programming

Treat data as streams that change over time — and declaratively wire how changes propagate.

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.

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.
RxJS (Angular, React, Node.js), Reactor (Java/Spring), Akka Streams (Scala), Combine (Swift), ReactiveX libraries in many languages.
📋 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.
Watch out: Reactive code has a steep learning curve — operators like switchMap, concatMap, and mergeMap are easy to confuse, and debugging a chain of transformations requires experience. Start with simple cases (debounce, merge two streams) before building complex pipelines.

Logic Programming

Describe facts and rules, then ask a question — the engine searches for values that satisfy them.

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.

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.
Prolog, Datalog, Answer Set Programming, constraint-logic hybrids (CLP(FD)).
📋 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
Watch out: Deep backtracking search can blow up exponentially on large fact bases. Order clauses carefully and use cuts (!) to prune dead-end branches, or queries can become unacceptably slow.

Aspect-Oriented Programming (AOP)

Pull cross-cutting concerns — logging, security, transactions — out of business logic into separate "aspects".

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.

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.
Java (Spring AOP, AspectJ), .NET (PostSharp), Python decorators as a lightweight everyday equivalent.
📋 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
Watch out: Woven behaviour is invisible at the call site — a method can suddenly log, retry, or authorise without any code nearby saying so, which makes control flow harder to trace. Use AOP sparingly and document pointcuts clearly.

Concurrent Programming

Structure a program as multiple tasks that can be in progress at the same time, coordinating shared resources safely.

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.

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.
Java threads, Go goroutines + channels, Python threading/asyncio, C# Task, Rust std::thread + Mutex.
📋 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();
  }
}
Watch out: Race conditions are notoriously hard to reproduce — they may only show up under load, in production, or on a different CPU. Prefer higher-level primitives (channels, actors, immutable data) over raw locks whenever possible.

Parallel Programming

Split one computation across multiple CPU cores (or machines) so it genuinely runs at the same time and finishes faster.

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.

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.
OpenMP / MPI (C/C++), Java Fork/Join, Python multiprocessing, GPU/CUDA kernels, Apache Spark.
📋 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
Watch out: More cores don't always mean linear speedup — coordination overhead, memory bandwidth limits, and Amdahl's Law mean returns diminish. Profile before assuming parallelising a hot loop will help.

Dataflow Programming

Model a program as a graph of nodes connected by data flowing through them — execution follows data availability, not a fixed order.

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.

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.
Unix pipes, spreadsheet formulas (Excel), Apache Beam, TensorFlow computation graphs, LabVIEW.
📋 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
Watch out: Debugging dataflow graphs is different from step-through debugging — you inspect data at each edge rather than following a call stack. Tooling with visual graph inspectors (Beam, TensorBoard) helps a lot.

Generic Programming

Write algorithms and data structures parameterised by type, so the same code works safely across many types.

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.

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.
C++ templates, Java/C# generics, TypeScript generics, Rust generics + traits, Go generics (1.18+).
📋 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;
}
Watch out: Over-generic APIs can become harder to read than a few concrete, duplicated functions. Reach for generics when the logic is truly type-independent, not just to avoid writing a second function.

Metaprogramming

Write code that generates, inspects, or modifies other code (or itself), at compile time or runtime.

Metaprogramming treats code as data that can be examined and manipulated programmatically — generating boilerplate, adding behaviour, or building DSLs without hand-writing every line.

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.
Python decorators + metaclasses, Rust macro_rules!/proc-macros, Lisp macros, Java annotation processors, TypeScript decorators.
📋 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
Watch out: Heavy metaprogramming (metaclasses, macro-generated code) can make debugging and IDE navigation painful — stack traces point into generated code, not what you wrote. Use it for genuine boilerplate elimination, not cleverness.

Structured Programming

Restrict control flow to sequence, selection, and iteration — no unconstrained jumps — so programs stay easy to reason about.

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

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.
The default in virtually every modern language (C, Java, Python, TypeScript) — structured control flow is simply assumed today.
📋 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;
}
Watch out: It's easy to forget this was ever controversial — Dijkstra's 1968 letter "Go To Statement Considered Harmful" is why virtually no mainstream language today even offers unrestricted GOTO.

Actor Model

Model concurrency as independent "actors" that only communicate by sending asynchronous messages — no shared mutable state.

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.

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.
Erlang/Elixir (OTP), Akka (Scala/Java), Microsoft Orleans (.NET).
📋 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
Watch out: Message passing trades shared-memory races for a different class of bugs: mailbox overload, out-of-order delivery, and needing explicit supervision strategies for failures. Design your message protocols carefully up front.

Prototype-Based Programming

Objects inherit directly from other objects (prototypes) instead of from classes.

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.

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.
JavaScript (under the hood, even when using class syntax), Lua (metatables), Self, Io.
📋 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
Watch out: JavaScript's class keyword is syntax sugar over this prototype chain — understanding prototypes explains surprising behaviour like methods being shared, not copied, across instances.

Constraint Programming

Declare constraints a solution must satisfy, and let a solver search the space of possibilities for you.

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.

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.
Google OR-Tools, MiniZinc, Z3 SMT solver, Prolog's CLP(FD) library — used for scheduling, routing, and configuration problems.
📋 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
Watch out: Constraint problems can be NP-hard — a solver may take exponential time on a large, tightly-constrained problem. Good variable/domain modelling and propagation rules matter more than raw solver speed.

Symbolic Programming

Manipulate symbols and expressions themselves — code as data — rather than only numeric or string values.

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.

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.
Lisp, Scheme, Clojure, Mathematica/Wolfram Language, symbolic-math libraries (SymPy).
📋 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
Watch out: The power to treat code as data (eval, macros) is also a foot-gun — dynamically evaluated strings/expressions can be hard to statically analyse, test, or secure if the source is untrusted.

Literate Programming

Interleave source code with natural-language explanation in one document, then extract runnable code and formatted docs from it.

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.

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.
Jupyter/R Markdown notebooks (a modern, informal descendant), Donald Knuth's original WEB/CWEB tools (used to write TeX itself).
📋 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 -->
Watch out: Full literate programming (Knuth-style) never went mainstream outside academia — most teams get 80% of the benefit from good docstrings/README files plus notebooks, without adopting a dedicated tangle/weave toolchain.

Data-Oriented Programming

Design around plain, immutable data and separate functions from data, optimising for data layout and transformations over object hierarchies.

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.

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.
Clojure (maps/vectors + specs), Entity-Component-System (ECS) architectures in game engines, Data-Oriented Design in C++/Rust game dev, Redux-style app state.
📋 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);
Watch out: Without the encapsulation OOP gives you, invariants on the data (e.g. "hp is never negative") must be enforced by every function that touches it, or centralised in a validation layer — nothing stops direct, unchecked mutation elsewhere.

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.