Routing Luau Paradigms
YAML--- name: routing-luau-paradigms description: Routes Luau/Lua system design decisions to the appropriate paradigm (OOP, Reactive, Declarative/Functional, or Event-driven) via a three-question decision tree. Use when choosing a paradigm, structuring a module, or reducing boilerplate for a new system. Triggers include "which paradigm", "OOP or functional", "combat system structure", "inventory architecture", "reactive UI". Do not use for syntax questions, API lookup, or performance tuning. ---
Routing Luau Paradigms
Answer three routing questions in strict order. Stop at the first "Yes". Fetch the linked reference before writing any code — do not rely on memory for API shape.
Given a task description, walk the tree:
- Does it own mutable state with identity/lifecycle? → OOP
- Else, does UI/dependent state need to auto-update on source change? → Reactive
- Else, is the core operation a pure data transform? → Declarative + Functional
- Else → Event-driven
Task: "Build a combat system with per-player cooldowns"
Q1: Yes (mutable state, identity = player) → OOP. Stop.
Progress:
- Restate the task in one sentence to confirm the "core operation"
- Ask Q1 (identity + lifecycle) — answer Yes/No explicitly
- If No, ask Q2 (auto-updating UI/dependents) — answer Yes/No explicitly
- If No, ask Q3 (pure transform, no side effects) — answer Yes/No explicitly
- Identify the leaf paradigm
- Fetch the reference link for that leaf before writing code
- Note any secondary paradigm needed only at boundaries (e.g., event-driven glue around an OOP class)
Q1 — Identity & lifecycle?
Examples: a specific Player instance, a persistent Tool, a stateful combat controller.
- Yes → OOP (fallback paradigm). Metatable +
__index, one ModuleScript per class. Typeselfexplicitly. Reference: https://luau.org/types/object-oriented-programs/ - No → Q2
Q2 — Auto-updating dependents?
UI or derived state that must stay in sync with a source value.
- Yes → Reactive. Declare state→UI binding once; always disconnect/cleanup on teardown. References: https://github.com/samnewmn/RoRx · UI: https://github.com/centau/vide
- No → Q3
Q3 — Pure transformation?
No side effects, input→output only.
- Yes → Declarative + Functional (used together).
- Use
map/filter/reduceover raw loops for collections; fetch https://github.com/Roblox/cryo - Keep side effects pushed to the edges of the system.
- Luau has no TCO — rewrite recursive algorithms as loops.
- Use
- No → Event-driven. Prefer
Deferredsignals; avoid rawBindableEvent/RemoteEventwiring unless a reactive abstraction is overkill. Always disconnect on cleanup. Reference: https://create.roblox.com/docs/scripting/events/deferred
Example 1:
Input: "Combat system with per-player cooldowns and state"
Output: Q1=Yes → OOP. Fetch luau.org OOP reference, build a Combat class with typed self, one per player.
Example 2: Input: "Inventory UI that reflects weight changes live" Output: Q1=No, Q2=Yes → Reactive. Bind weight state to UI display via RoRx/Vide; clean up subscription on UI unmount.
Example 3:
Input: "Damage calculation from stats and equipment"
Output: Q1=No, Q2=No, Q3=Yes → Declarative + Functional. Pure function calculateDamage(stats, equipment) -> number; use cryo helpers if composing over collections.
Example 4:
Input: "Cross-server matchmaking notification"
Output: Q1=No, Q2=No, Q3=No → Event-driven. Use Deferred signal, disconnect listener on match completion.
Example 5: Input: "Plain inventory data, no UI, no identity" Output: Q1=No, Q2=No, Q3=Yes → Declarative + Functional.
- Run the questions in order; never jump to a leaf based on intuition alone.
- State the leaf explicitly ("→ OOP") before writing any code.
- Mixing paradigms is normal — the leaf is the primary paradigm for the core module; secondary paradigms belong only at system boundaries (e.g., an event-driven signal notifying an OOP controller).
- When two leaves both seem defensible, pick whichever removes more boilerplate for this specific task.
- Always fetch the reference link for the chosen leaf — these are living libraries/docs, not stable enough to recall from memory.
- Skipping straight to OOP because "that's how Roblox code usually looks" — identity/lifecycle must be genuinely present, not assumed.
- Choosing Reactive for one-off state reads that don't need to auto-propagate — that's just a function call.
- Using recursion for "pure transform" leaf without checking for TCO — Luau will stack overflow on deep recursion.
- Wiring raw
BindableEvent/RemoteEventfor event-driven leaf whenDeferredsignals are available and more appropriate. - Forgetting cleanup/disconnect logic in Reactive and Event-driven leaves — this is the most common leak source in both.