[Tutorial] From OOP to Data-Oriented Design: Memory layout, AoS vs SoA, FIFO/LIFO

[0] Introduction



Hello everyone!
In this tutorial, I want to introduce you to an important way of thinking in programming called Data-Oriented Design (DOD) and explain some fundamental concepts such as AoS vs SoA, FIFO/LIFO in a simple and beginner-friendly way.

Most Roblox developers learn programming using Object-Oriented Programming (OOP) — classes, objects, and methods. OOP is very useful and convenient, especially for UI, tools, and general gameplay logic.
However, when it comes to performance-critical systems (signals, combat systems, ECS, animations, networking, or large update loops), OOP can become inefficient in Roblox because it relies heavily on tables, metatables, and many small objects.

This is where Data-Oriented Design comes in.

Instead of focusing on objects, DOD focuses on data and how it is stored and processed in memory.
The main idea is simple:

Don’t ask “What does this object do?”
Ask “What data do I need to process as fast as possible?”

In this tutorial, we will cover:

  • What Data-Oriented Design (DOD) is and how it differs from OOP
  • The difference between Array of Structures (AoS) and Structure of Arrays (SoA)
  • What FIFO and LIFO mean and where they are used
  • Where these concepts can be useful in Roblox projects

This tutorial is not about replacing OOP everywhere.
OOP is still great for many parts of a game. The goal is to show you another tool and another way of thinking that can help you write faster and more efficient code when performance really matters.

If you are curious about how to make your systems faster and want to understand how professional game engines think about data, then this tutorial is for you. :rocket:

Let’s start by comparing Object-Oriented Programming and Data-Oriented Design.

[1] OOP vs DOD, What is a DOD and why its exsits?



Before we talk about AoS, SoA, and Ring Buffers, we need to understand the difference between two ways of thinking about code:
Object-Oriented Programming (OOP) and Data-Oriented Design (DOD).

These are not enemies. They are just different tools for different problems.


:green_circle: What is OOP?

In Object-Oriented Programming, we organize our code around objects.
Each object contains:

  • data (fields / properties)
  • behavior (methods / functions)

Example (OOP-style):

local SomeObject = {}
SomeObject.__index = SomeObject

function SomeObject.new(x, y, hp)
    return setmetatable({
        X = x,
        Y = y,
        HP = hp
    }, SomeObject)
end

function SomeObjectr:Move(dx, dy)
    self.X += dx
    self.Y += dy
end

function SomeObject:TakeDamage(amount)
    self.HP -= amount
end

Here, each SomeObject is its own table with its own data and methods.
This is very easy to read and very intuitive.

OOP is great for:

  • UI systems
  • tools and editors
  • game logic
  • small to medium systems
  • code readability

:blue_circle: What is DOD?

In Data-Oriented Design, we organize our code around data, not objects.
Instead of having many small objects, we store the same type of data in arrays and process them in simple loops.

Example (DOD-style):

local X = {}
local Y = {}
local HP = {}

local function MoveEntity(i, dx, dy)
    X[i] += dx
    Y[i] += dy
end

local function DamageEntity(i, amount)
    HP[i] -= amount
end

Here, an “entity” is just an index i that refers to data in multiple arrays.
There is no SomeObject object - only data.

DOD is great for:

  • large numbers of entities
  • update loops
  • combat systems
  • ECS
  • signals and events
  • performance-critical code

:balance_scale: Main Difference

The main difference is what you focus on first:

OOP DOD
Objects Data
Methods Loops
Many small tables Few big arrays
Easy to read Easy to optimize
Flexible Fast

OOP asks:

“What can this object do?”

DOD asks:

“What data do I need to process quickly?”


:puzzle_piece: A Simple Comparison

Imagine you want to move 1000 objects.

OOP-style:

for _, object in ipairs(object) do
    object:Move(1, 0)
end

Each loop:

  • accesses a table
  • calls a method
  • looks up fields

DOD-style:

for i = 1, count do
    X[i] += 1
end

This loop:

  • works on raw arrays
  • has fewer table lookups
  • is easier for the engine to optimize

This is why DOD is often faster in hot loops.

[2] Memory layout, AoS vs SoA


This is very impotant section, dont skip any word.

What is a memory layout?

Memory layout - How stored our data in RAM and how easily CPU getting it.
More efficient memory layout = more easier to CPU.

There are two common ways to organize data are:

  • AoS - Array of Structures
  • SoA - Struct of Arrays

:green_circle: 2.1 What is AoS (Array of Structures)

AoS means that each entity is stored as a single table that contains all its data.

Example (AoS style):

local Entities = {}

Entities[1] = { X = 0, Y = 0, HP = 100 }
Entities[2] = { X = 10, Y = 5, HP = 80 }
Entities[3] = { X = -3, Y = 2, HP = 120 }

Each element in the array is a structure (table) with multiple fields.

This is very similar to how OOP objects are usually stored.

:white_check_mark: Pros of AoS:

  • Easy to read
  • Easy to debug
  • Natural for OOP thinking
  • Good for small systems

:red_exclamation_mark: Cons of AoS:

  • Many table lookups (entity.X, entity.Y, entity.HP)
  • Poor cache locality
  • Slower when processing large amounts of data
  • More memory overhead

:blue_circle: 2.2 What is SoA (Structure of Arrays)

SoA means that each type of data is stored in its own array.

Example (SoA style):

local X = {}
local Y = {}
local HP = {}

X[1], Y[1], HP[1] = 0, 0, 100
X[2], Y[2], HP[2] = 10, 5, 80
X[3], Y[3], HP[3] = -3, 2, 120

Here, the entity is just an index (1, 2, 3) across multiple arrays.

:white_check_mark: Pros of SoA:

  • Fewer table lookups
  • Better cache locality
  • Faster in loops
  • Easier to optimize
  • Lower memory overhead

:red_exclamation_mark: Cons of SoA:

  • Harder to read
  • Harder to debug
  • More manual management
  • Less intuitive at first

2.3 AoS vs SoA Comparison

Feature AoS SoA
Readability :white_check_mark: Easy :cross_mark: Harder
Performance :cross_mark: Slower :white_check_mark: Faster
Memory usage :cross_mark: Higher :white_check_mark: Lower
Cache locality :cross_mark: Poor :white_check_mark: Good
Debugging :white_check_mark: Easy :cross_mark: Harder
Best for Small systems, UI Large systems, hot loops

:test_tube: 2.4 Practical Example: Moving Entities

Let’s compare how we move many entities in both approaches.

AoS version:

for i = 1, count do
    local entity = Entities[i]
    entity.X += 1
    entity.Y += 1
end

Each iteration:

  • accesses a table
  • looks up fields
  • updates values

SoA version:

for i = 1, count do
    X[i] += 1
    Y[i] += 1
end

Each iteration:

  • accesses raw arrays
  • has fewer lookups
  • is more cache-friendly

This is why SoA is often much faster when count becomes large.


:brain: 2.5 Why SoA fits Roblox well

Roblox’s Luau VM is optimized for:

  • numeric indices
  • arrays
  • simple loops
  • local variables

It is not optimized for:

  • deep object hierarchies
  • many small tables
  • metatables in hot loops

SoA matches very well with how Luau executes code.

[3] FIFO vs LIFO and what is this?



LIFO and FIFO is a ways how we can organize Queue in our games/libraries

FIFO (First In, First Out) - the first element added is the first one removed, Example: a line in a store
LIFO (Last in, First Out) - the last element added is, the firest one removed, example: Stack of plates

:green_circle: LIFO (Stack)

Simple stack example:

local stack = {}

-- push
stack[1] = "A"
stack[2] = "B"
stack[3] = "C"

-- pop 
local last = table.remove(stack, #stack)

print("Removed:", last) -- log: C

Used for:

  • undo systems
  • temporary data
  • recursion
  • history

:blue_circle: FIFO (Queue)

Naive queue (slow):

local queue = {}

local queue = {}

-- enqueue
queue[1] = "A"
queue[2] = "B"
queue[3] = "C"

-- dequeue
local first = table.remove(queue, 1)

print("Removed:", first)
print("Now queue:", table.concat(queue, ", "))

This is slow because all elements are shifted.

Used for:

  • task queues
  • events
  • networking
  • job systems

:white_check_mark: Summary

  • LIFO = stack (last in, first out)
  • FIFO = queue (first in, first out)
  • table.remove(queue, 1) is slow, but we solve this in next section

[4] Some Techniques to make your code better


Post is too long, i will only make a links to a wikipedia

Ring buffer - Circular buffer - Wikipedia
Swap-Remove - O(1) method to make FIFO Queue fast, but it makes it Unordered
Batching - Batch processing - Wikipedia
Sentinels - Sentinel value - Wikipedia

How make Swap-Remove?
Example:

local queue = table.create(10)

for i = 1, 10 do
    queue[i] = i
end

local swap_remove = function(index: number)
    local last = #queue
    queue[index] = queue[last]
    queue[last] = nil
end
swap_remove(4)

-- what we get:
-- 1, 2, 3, 10, 5, 6, 7, 8, 9,

mini advices:

  • use counters instead #array
  • use table.create(number, sentinel value)
  • use shared refs like a Sentinels

Example of a shared ref:

local NULL = table.freeze({})

local array = table.create(10, NULL) -- all elements in array is a pointer to a shared ref
-- this is a very advanced sentinel!

-- we can check this
local isEmpty = array[1] == NULL-- true
array[1] = 123
local isEmpty = array[1] == NULL -- false

This is end of my Post. Happy coding! :baby_angel:

19 Likes

AoS is still object orientated, isnt it?

1 Like