Deep Call Chains vs Orchestrator Pattern
You open a file to trace a bug. You find handleSubmit, which calls validateForm, which calls sanitizeInput, which calls checkPermissions, which calls saveToDatabase. Five levels deep and you still don't know where the bug is.
This is the deep call chain problem, a common design smell in everyday code. The fix is simple: stop letting functions decide what runs next.
The Problem: Deep Call Chains
A deep call chain happens when function A calls function B, which calls C, which calls D. Each function owns its own logic and also triggers the next step.
At first glance, this looks organized. Each function has a "single responsibility." But look closer: serious problems hide in this structure.
Why This Hurts
-
You can't follow the flow. To understand what
handleSubmitdoes, you open five functions across maybe five files. The execution path is buried inside nested calls. -
You can't test in isolation. Want to test
sanitizeInput? You can't, because it callscheckPermissions, which callssaveToDatabase. Every test for one function tests the whole chain. -
You can't reuse anything. Need to sanitize input without checking permissions? Too bad, every function is coupled to the one it calls.
-
You can't change the order. What if a new requirement checks permissions before validation? You'd rewire the internals of multiple functions.
When functions call each other in a chain, you create a hidden workflow scattered across your codebase. Nobody sees the full picture without tracing every call.
The Fix: Let One Function Own the Flow
Introduce a single function that knows the order. Each step becomes an independent function that does one thing and returns. One function at the top decides what runs, in what order.
Now look at what changed:
- The flow is visible. Read
handleSubmitand you see the full workflow: check permissions, validate, sanitize, save. - Each function is independent.
sanitizeInputknows nothing about permissions or databases. Test it with a simple input/output assertion. - Reordering is trivial. Need to validate before checking permissions? Swap two lines.
- Reuse is free. Need
sanitizeInputelsewhere? Just call it, it has no hidden dependencies.
Functions should not decide what runs next. The orchestrator decides the order.
Key Takeaways
-
Functions should do work, not decide what comes next. One function holds the workflow; individual functions hold the logic.
-
Flat beats deep. A 10-line orchestrator calling 5 independent functions is easier to understand than 5 functions calling each other in a chain.
-
Testability follows naturally. When functions don't call each other, you test each one with simple inputs and outputs, no mocking required.
Next time you write a function that calls another function that calls another function, stop and ask: should this function know about the next step, or should someone else make that decision?