Types of Programming

Data-Oriented Design

Organising code around data layout and transformation for cache efficiency.

Overview

Data-Oriented Design (DOD) organises programs around the data that needs to be transformed, not the objects or procedures that transform it. Popularised in game development by Mike Acton (2014 CppCon talk), DOD restructures data for cache-friendly access patterns: arrays of structs (AoS) vs structs of arrays (SoA), hot/cold data splitting, and avoiding pointer chasing.

Origin

Emerged from game engine development in the 2000s as games pushed hardware limits. Mike Acton's work at Insomniac Games, and later Scott Meyers' discussion of CPU cache effects, codified the approach. Unity's ECS (Entity Component System) and DOTS (Data-Oriented Technology Stack, 2018) brought it into mainstream game tooling.

Examples

Struct of arrays vs array of structs in TypeScript

// Array of Structs (AoS) - typical OOP layout
interface ParticleAoS {
  x: number; y: number; z: number;
  vx: number; vy: number; vz: number;
  mass: number; active: boolean;
}
const particlesAoS: ParticleAoS[] = new Array(10000).fill(null).map(() => ({
  x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, mass: 1, active: true
}));

// Struct of Arrays (SoA) - cache-friendly for per-field operations
interface ParticlesSoA {
  x: Float32Array; y: Float32Array; z: Float32Array;
  vx: Float32Array; vy: Float32Array; vz: Float32Array;
  mass: Float32Array; active: Uint8Array;
}
const N = 10000;
const particles: ParticlesSoA = {
  x: new Float32Array(N), y: new Float32Array(N), z: new Float32Array(N),
  vx: new Float32Array(N), vy: new Float32Array(N), vz: new Float32Array(N),
  mass: new Float32Array(N).fill(1), active: new Uint8Array(N).fill(1),
};

// Update positions: SoA reads x, vx contiguously, one cache miss per cache line
function updatePositions(p: ParticlesSoA, dt: number) {
  for (let i = 0; i < N; i++) {
    p.x[i] += p.vx[i] * dt;
    p.y[i] += p.vy[i] * dt;
    p.z[i] += p.vz[i] * dt;
  }
}

On a CPU with 64-byte cache lines and 4-byte floats, SoA loads 16 x-values per cache line. AoS loads 1 x-value and 7 fields we do not need. For 10,000 particles, SoA uses ~12x fewer cache misses for this loop.

Hot/cold data splitting for entity processing

// Cold data: rarely accessed metadata
interface EntityCold {
  id: string;
  name: string;
  createdAt: Date;
  tags: string[];
  description: string;
}

// Hot data: accessed every frame/tick
interface EntityHot {
  x: number;
  y: number;
  velocityX: number;
  velocityY: number;
  hp: number;
}

// Separate hot and cold arrays so physics loop fits in L1/L2 cache
const hotData: EntityHot[] = [];
const coldData: EntityCold[] = [];

function physicsUpdate(hot: EntityHot[], dt: number) {
  for (const e of hot) {
    e.x += e.velocityX * dt;
    e.y += e.velocityY * dt;
  }
  // coldData is never touched here; stays cold in memory
}

Physics runs every 16ms; cold metadata (name, description) is needed only on entity creation and UI rendering. Separating them keeps the hot loop's working set small enough to fit in L1 cache (typically 32-64KB).

Use Cases

  • 01Game engines processing thousands of entities per frame where cache miss latency (100-300 cycles) dominates update time
  • 02Scientific simulations (fluid dynamics, particle systems) with large homogeneous datasets and tight inner loops
  • 03Database storage engines where row vs column orientation (OLTP vs OLAP) is the same AoS/SoA tradeoff
  • 04WebGL and WebGPU shader pipelines where data must be uploaded to GPU in tightly packed typed arrays

When Not to Use

  • //Business applications where developer productivity and domain clarity matter more than the last 10% of throughput
  • //Heterogeneous entity types with very different attribute sets where SoA layout wastes memory on sparse arrays
  • //When profiling has not identified memory access as the bottleneck; premature data-layout optimisation is as harmful as any other premature optimisation

Technical Notes

  • CPU cache hierarchy on a modern x86: L1 ~32KB 4-cycle latency, L2 ~256KB 12-cycle, L3 ~8MB 40-cycle, DRAM 60ns (~200 cycles). A cache miss to DRAM costs 50x more than an L1 hit
  • Unity DOTS uses Burst Compiler (LLVM-based) to compile C# job system code with SIMD (AVX2) auto-vectorisation; this requires SoA layout to vectorise multiple entities in a single instruction
  • Column-oriented databases (ClickHouse, Parquet, DuckDB) apply the SoA insight to disk storage; a query touching two of twenty columns reads 10% of the data a row-oriented store would read
  • Entity Component System (ECS) is the architectural pattern that operationalises DOD: entities are integer IDs; components are arrays of data; systems iterate over component arrays. Flecs (C), EnTT (C++), and bevy_ecs (Rust) are implementations