Programming Paradigms

Six fundamental approaches to writing software — explained in plain English, with real vocabulary, code examples, and guidance on when each one shines.

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.