luaguides

Mixins and Composition Patterns in Lua

Mixins and composition offer a way to add capabilities to objects in Lua without the constraints of classical inheritance. A mixin is a table containing methods that you attach to another table, establishing a has-a or can-do relationship rather than an is-a relationship. Instead of building rigid inheritance hierarchies, you compose objects from reusable chunks of functionality.

This matters because Lua’s table-based object model makes mixins natural to implement. Whether you need to add logging to unrelated classes, combine capabilities from multiple sources, or build libraries of composable behavior, mixins give you that flexibility.

Prerequisites

Before working through this tutorial, you should understand how Lua tables store key-value pairs and how they serve as the foundation for all data structures in the language. You should also be comfortable with metatable basics covered in earlier tutorials in this series, particularly how __index delegates field lookups. Familiarity with Lua’s colon syntax for method calls (obj:method() is sugar for obj.method(obj)) will help you follow the code examples.

The simplest mixin: single metatable delegation

The most straightforward approach uses __index on a metatable to delegate lookups to the mixin:

local Flyable = {}
function Flyable:fly()
    return "I can fly!"
end

local Bird = {}
setmetatable(Bird, { __index = Flyable })

print(Bird:fly())  -- "I can fly!"

When Lua looks for fly on Bird and doesn’t find it, the metatable’s __index redirects the lookup to Flyable. The Bird object gains the fly method without any explicit inheritance declaration.

This pattern works well for a single mixin. The limitation is that you only get one fallback table, so complex objects with many capabilities need a different approach.

Multiple mixins with custom __index

When you need to attach more than one mixin, use a custom __index function that searches through several tables. Each mixin contributes its methods, with later mixins overriding earlier ones when names collide.

Unlike simple metatable delegation that points to a single fallback table, the applyMixins approach builds a merged index from multiple mixin tables. It processes each mixin using select("#", ...) to count how many variadic arguments were passed, then iterates through them with select(i, ...). The guard if not index[k] implements first-wins collision detection: when two mixins define a method with the same name, the first one encountered keeps its implementation. This preserves the caller’s intent when the argument order matters.

local function applyMixins(class, ...)
    local index = {}
    for i = 1, select("#", ...) do
        local mixin = select(i, ...)
        for k, v in pairs(mixin) do
            if not index[k] then
                index[k] = v
            end
        end
    end
    setmetatable(class, { __index = index })
end

local Flyable = {}
function Flyable:fly() return "flying" end

local Swimmable = {}
function Swimmable:swim() return "swimming" end

local Duck = {}
applyMixins(Duck, Flyable, Swimmable)

print(Duck:fly())   -- "flying"
print(Duck:swim())  -- "swimming"

select("#", ...) counts the variadic arguments so the function can iterate through each mixin. The if not index[k] check prevents earlier mixins from being overwritten by later ones with the same method name.

The applyMixins approach uses first-wins ordering: whichever mixin appears first in the argument list claims a method name, and later mixins with the same name are ignored. This works well when you want a default mixin that takes priority. The PriorityMeta approach below flips this: it searches mixins in reverse so the last-listed mixin wins. Last-wins is more natural when you think of later arguments as overriding earlier defaults. Choose first-wins for additive layering and last-wins for override-style composition.

Controlling priority with reverse lookup

Sometimes you want explicit control over which mixin wins in a conflict. A wrapper object that stores mixins in order and searches them in reverse gives you that:

local PriorityMeta = {}
function PriorityMeta.__index(tbl, key)
    for i = #tbl._mixins, 1, -1 do
        local mixin = tbl._mixins[i]
        if mixin[key] then
            return mixin[key]
        end
    end
end

local function withMixins(obj, ...)
    local wrapper = { _mixins = { ... } }
    return setmetatable(wrapper, PriorityMeta)
end

With this wrapper, the last mixin in the argument list has highest priority because PriorityMeta.__index iterates through _mixins from the end to the beginning. Each lookup walks the list in reverse, returning the first matching key it finds. Since later arguments appear at higher indices, they get checked first and win any conflicts. This gives the caller an intuitive mental model: pass your base mixins first, then pass overrides last.

local Speak = { speak = function() return "speaking" end }
local Sing = { speak = function() return "singing" end }

local human = withMixins({}, Speak, Sing)
print(human.speak()) -- "singing"

The reverse iteration (#tbl._mixins, 1, -1) means Sing’s method gets checked first and wins.

With both delegation-based approaches covered, the next technique takes a different route: instead of looking up methods through a metatable on every call, you can copy them directly onto the target once and be done with it.

Method copying for baked-in behavior

An alternative to delegation is copying methods directly into the target object. This “bakes in” the behavior permanently:

local function mix(target, mixin)
    for k, v in pairs(mixin) do
        if not target[k] then
            target[k] = v
        end
    end
    return target
end

local Printable = {}
function Printable:print()
    print(self.value)
end

local Thing = { value = "hello" }
mix(Thing, Printable)

Thing:print() -- "hello"

The guard if not target[k] prevents overwriting existing keys. Once copied, the methods belong to the object and persist even if you change the metatable later.

Method copying differs from delegation in a key way: copied methods become own properties of the target rather than lookups through a metatable. This means they appear when you iterate with pairs(), survive metatable reassignment, and can be individually overridden per instance. With delegation, methods live on the mixin table and disappear if the metatable or its __index changes. Choose copying when you need methods to stay with an object permanently; choose delegation when you want to swap or remove mixin behavior at runtime.

Mixin factories for reusable libraries

When you need the same mixins across multiple projects or want to provide instances with shared behavior, a factory pattern keeps things organized.

Unlike a simple mixin table that you attach directly to objects, a factory wraps the mixin in a module that returns a table with its own __index pointing back to itself. When you call setmetatable(instance, Factory) on a factory table, any method lookup that misses the instance falls through to the factory’s methods. This means all instances share the same behavior through metatable delegation, while each instance keeps its own data. The factory pattern also makes mixins proper Lua modules: you require() them once, call .new() for fresh instances, or attach them to existing objects via setmetatable. This keeps mixin code isolated, testable, and reusable across any project that needs it.

-- Serializable.lua
local Serializable = {}
Serializable.__index = Serializable

function Serializable.new(instance)
    return setmetatable(instance or {}, Serializable)
end

function Serializable:serialize()
    local parts = {}
    for k, v in pairs(self) do
        if type(v) ~= "function" then
            table.insert(parts, k .. "=" .. tostring(v))
        end
    end
    return "{" .. table.concat(parts, ",") .. "}"
end

return Serializable

To use the factory, you require() the module and call .new() to create an instance already wired to the mixin’s methods. The .new() function accepts an optional existing table, letting you retrofit serialization onto any object. You can also skip the factory entirely and attach the mixin with setmetatable(obj, { __index = Serializable }), which gives you the same method delegation without going through .new().

local Serializable = require("Serializable")

local Point = Serializable.new({ x = 10, y = 20 })
print(Point:serialize())  -- "{x=10,y=20}"

Each instance gets its own state while delegating to the shared mixin methods. The Point object stores x and y directly, but serialize() lives on the shared mixin table. You can also add the mixin to existing objects without going through the factory:

local Vector = { x = 1.5, y = 3.7 }
setmetatable(Vector, { __index = Serializable })

print(Vector:serialize())  -- "{x=1.5,y=3.7}"

The factory pattern shows how mixins scale beyond single files into reusable libraries. The next example brings everything together: two unrelated game entity types each need logging, and each could also use serialization, but neither needs to share a parent class. This is the core use case for mixins and composition — attaching capabilities horizontally across unrelated types.

Real-world example: mixins and composition with loggable and serializable

Imagine building a game where several unrelated classes need the same capabilities:

local Loggable = {}
function Loggable:log(msg)
    print("[LOG] " .. tostring(msg))
end

local Serializable = {}
function Serializable:serialize()
    local parts = {}
    for k, v in pairs(self) do
        if type(v) ~= "function" and type(v) ~= "table" then
            table.insert(parts, k .. "=" .. tostring(v))
        end
    end
    return "{" .. table.concat(parts, ",") .. "}"
end

local Player = { name = "Arin", health = 100, mana = 50 }
setmetatable(Player, { __index = Loggable })
Player.log("Player loaded")  -- "[LOG] Player loaded"

local Enemy = { type = "goblin", health = 30 }
setmetatable(Enemy, { __index = Loggable })
Enemy.log("Enemy spawned")  -- "[LOG] Enemy spawned"

The same Loggable mixin works on Player and Enemy even though they share no inheritance relationship. You can attach different combinations of mixins to different classes depending on what each needs.

Common Pitfalls

Method resolution order matters when mixins have overlapping method names. The last mixin in your list wins. In applyMixins(Duck, Flyable, Swimmable), if both define an action method, Swimmable’s version handles the call. Be explicit about the order when you care about which one wins.

Shared method tables mean that simple metatable delegation shares the same function objects across all instances. If a mixin method closes over mutable state, modifying it affects every object using that mixin. Your instance data lives in the object itself (self), but the methods are shared. This is usually fine unless you need to modify the mixin’s behavior at runtime for individual instances.

Name collisions can silently overwrite methods you didn’t expect to replace. The copying approach with its if not target[k] guard protects against this, but metatable delegation without a custom __index will happily override any existing key. When combining multiple mixins, audit which methods each provides.

Mixins and super don’t mix. Classical inheritance gives you super calls to invoke parent implementations. With mixins, you lose that chain. If you need to call an original implementation after modifying behavior, consider composition with explicit delegation instead of mixin attachment.

When to use mixins vs inheritance

Mixins excel at horizontal composition: adding the same capability to unrelated classes. If you have logging, serialization, or validation that many different classes need, mixins avoid duplicating code across an inheritance tree.

Inheritance makes sense when you have a genuine is-a hierarchy with overridden behavior that needs a common base. A Dog is-an Animal, so inheritance fits. But if you need a Robot that can move and speak but isn’t really an Animal, mixins let you compose exactly those capabilities without forcing a taxonomy.

In practice, Lua projects often use both. You might have an inheritance chain for core entity types, then layer mixins on top for optional features like save/load or debug logging.

Conclusion

Mixins and composition give you flexible composition in Lua without inheritance’s constraints. Start with simple metatable delegation for single mixins, scale to custom __index chains when you need multiple mixins, and consider method copying when you want behavior baked in permanently. For larger projects, factory patterns keep mixin libraries organized and reusable.

The right pattern depends on whether you need dynamic priority ordering, static baked-in behavior, or organized mixin modules. Start simple and add complexity only when your use case demands it.

Next steps

The patterns shown here cover the most common mixin approaches in Lua. Try combining method copying with factory functions to build your own mixin library, then experiment with the priority ordering approaches to see which fits your use case. When you are ready to go deeper into object-oriented Lua, the inheritance tutorial covers classical hierarchies and how they interact with the composition patterns from this tutorial.

See Also