luaguides

Mixins and Composition Patterns in Lua

Mixins 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.

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.

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.

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, the last mixin in the argument list has highest priority:

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.

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 subtle way: the methods become own properties of the target rather than lookups through a metatable. This matters when you want to inspect or modify the object’s own capabilities 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:

-- 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

Usage:

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. You can also add the mixin to existing objects:

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

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

Real-World Example: 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 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.

See Also

Written

  • File: sites/luaguides/src/content/tutorials/mixins-and-composition.md
  • Words: ~900
  • Read time: 4 min
  • Topics covered: mixins, composition, metatable delegation, multiple mixins, method copying, mixin factories, when to use vs inheritance
  • Verified via: research.md (patterns verified against Lua 5.4 stdlib docs)
  • Unverified items: none