Data-Oriented Design, Without the Dogma
Organise data around how it is processed rather than around how the domain is modelled. It is a real technique with real wins in hot loops — and a genuinely bad default for code where clarity matters more than cache lines.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
What the technique actually is
Data-oriented design starts from a different question than object-oriented modelling. Rather than asking what an entity *is*, it asks what transformations run over the data, how often, and in what order — then chooses a layout that serves those transformations. The layout decisions that follow are usually the ones in Array of Structs, or Struct of Arrays? and Padding: Why Your Struct Is Bigger Than Its Fields: group by field rather than by entity, split hot from cold, and prefer flat contiguous storage to graphs of references.
The second, less discussed half is about batching. Processing a thousand entities in one pass over dense arrays keeps instruction cache and data cache both warm and gives the branch predictor a consistent pattern. Processing them one at a time through a virtual dispatch scatters both. The technique is as much about the shape of the loop as about the shape of the data.
The compare below is the canonical transformation: a polymorphic per-entity update becomes a batched pass over dense arrays.
1for (entity in entities) { // array of pointers2 entity.update(dt) // virtual dispatch3}4 5// per iteration:6// - load the pointer (one line)7// - load the object (another line, scattered on heap)8// - load the vtable (another line)9// - indirect call (often mispredicted)10// - touch all fields, use two (wasted line fraction)1// positions and velocities in separate dense arrays2for (i = 0; i < n; i++) {3 pos_x[i] += vel_x[i] * dt4 pos_y[i] += vel_y[i] * dt5}6 7// per iteration:8// - contiguous loads, prefetcher runs ahead9// - every fetched byte used10// - no indirect call, so nothing to mispredict11// - straightforwardly vectorisableThe second version does the same arithmetic. What it removes is three indirections, an unpredictable indirect branch, and most of the wasted cache-line fraction — while making the loop a candidate for vectorisation. That combination is why the technique produces large wins in simulation and numeric code.
Where it earns its cost, and where it does not
The honest accounting is that data-oriented design trades abstraction for throughput. Splitting entities into parallel arrays means there is no longer one place that represents a particle; creating and destroying entities requires keeping several arrays consistent; and the code reads as loops over indices rather than as domain operations. That is a real and ongoing maintenance cost.
It buys, in the right circumstances, several-fold throughput improvements. So the question is entirely about proportion: what fraction of runtime does this code account for, and how often does it change? A physics inner loop running millions of times per frame is worth restructuring. A configuration parser, an admin endpoint, or a workflow that runs once per request is not, no matter how satisfying the transformation would be.
The failure mode worth naming is applying it as an identity rather than a tool — restructuring an entire codebase around cache lines when a profiler would show that ninety-five percent of it never appears in a hot path. That is the same error as premature optimisation wearing different clothes, and it costs the clarity that lets you find the actual bottleneck later.
| Situation | Worth it? | Reasoning |
|---|---|---|
| Inner loop over millions of entities, runs constantly | Yes | Layout dominates runtime; the maintenance cost is repaid continuously |
| Batch job scanning a large dataset | Yes | Bandwidth-bound; useful-line fraction is the limiting factor |
| Code the profiler shows as under 1% of runtime | No | Even a 10x win is invisible; you pay clarity for nothing |
| Business logic that changes weekly | No | Maintenance cost is paid repeatedly, performance benefit is negligible |
| Code with complex, evolving domain rules | Rarely | Abstraction is doing real work; flattening it makes change expensive |
| A hot loop inside otherwise ordinary code | Yes, locally | Restructure the loop and its data only, leave the rest alone |
The transferable idea
Strip away the movement and the advocacy and one durable insight remains: layout is a design decision with performance consequences, and it is usually made implicitly. Most code inherits its layout from how the domain was modelled, without anyone asking what the hot loops will need. Simply making the decision consciously — even if you conclude the current layout is fine — is most of the value.
The second transferable idea is that this thinking scales beyond memory. Columnar database storage is data-oriented design at disk scale. Batching in a network protocol is the same instinct applied to round trips. Vectorised query execution in a database engine is precisely the "process many, not one" pattern. The mechanism differs; the reasoning is identical.
So the useful stance is neither adoption nor dismissal. It is: know the technique, know what it costs, profile before applying it, and apply it locally where the measurement justifies it. That is the same discipline this whole domain asks for — understand the machine, then decide deliberately rather than by default.
- Make layout an explicit decision rather than an accident of domain modelling.
- Apply locally to measured hot loops, not globally as an architectural style.
- The same reasoning appears as columnar storage, batching and vectorised execution elsewhere.
- Abstraction has value that does not appear in a profile; spending it should be a deliberate trade.
Key points
- Data-oriented design means choosing layout to serve the transformations that run over the data, rather than the domain model.
- Its two halves are dense contiguous layout and batched processing; the second matters as much as the first.
- It trades abstraction and maintainability for throughput, which is a good trade only where the code is genuinely hot.
- Applied globally as a style it is a costly mistake; applied locally to measured hot loops it is among the largest available wins.
- The durable insight is that layout is a design decision usually made by accident — making it consciously is most of the benefit.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Domain modelling → layout: entities become objects, so a hot loop's fields end up scattered across records and heap allocations.
- 2Scattered fields → cache lines: each iteration touches several lines and uses a small fraction of each.
- 3Indirection → dependent loads: pointer-based entity access adds dependent loads and unpredictable indirect branches.
- 4Restructure → dense arrays: fields the loop needs become contiguous, so every fetched byte is consumed.
- 5Batched loop → hardware: constant stride enables prefetching, removes indirect calls and permits vectorisation.
- • "Object-oriented code is slow." Indirection and scattered layout are slow. Objects with dense layout in cold code cost nothing worth measuring.
- • "This should be the default style." It is a targeted optimisation. Used as a default it spends clarity everywhere to gain speed in a few places.
- • "The transformation is obviously correct, so it does not need measuring." Layout effects depend on cache sizes and working-set size; verify rather than assume.
Consequences, controls and cost
- • Hot loops restructured this way commonly show several-fold throughput improvements with identical arithmetic.
- • Codebases restructured wholesale become markedly harder to change, usually for negligible aggregate gain.
- • Entity creation and deletion become more complex once fields live in parallel arrays.
- • Profile first, and restructure only the loops that measurement shows dominate runtime.
- • Apply the transformation locally, keeping domain-shaped code at the boundaries of the hot region.
- • Start with the cheapest variants — hot/cold field splitting and field reordering — before full decomposition.
- • Re-measure afterwards; layout changes interact with cache sizes and can fail to help or even regress.
- • Profile to establish which loops actually dominate runtime before restructuring anything.
- • Measure cache misses and achieved bandwidth before and after, not just wall time, so you know why it changed.
- • Check whether the restructured loop vectorised, using compiler reports rather than inference.
- • Abstraction is lost: there is no longer a single place representing an entity, which complicates every future change.
- • Creation, deletion and invariant maintenance across parallel arrays are error-prone in ways a single struct is not.
- • The layout is tuned to today's access pattern, so a new access pattern can invalidate the whole arrangement.
Scope
§224 — what these claims are specific to.
- GENERALThe reasoning applies to any cache-based machine. The size of the win is MICROARCH-SPECIFIC and depends on cache sizes and working-set size relative to them.
Misconceptions
Apply it
Where the rest of this lives
How much indirection an entity access costs is decided by the language and runtime — vtable dispatch, boxing and object headers all add loads that a flat array does not have.