Notes · Software Architecture
budding
Your Application Is a Graph
Dependency injection becomes easier to reason about when wiring is treated as a finite requirements graph: compile what is known, preserve genuine runtime choices, and keep authorization separate.
It is the function of these next levels to build the machine that has been assumed to be available at the top level.
— Edsger W. Dijkstra, “Concern for Correctness as a Guiding Principle for Program Composition”
Cite this
Mangalapilly, Y. J. (2026, August). Your Application Is a Graph. Saṃhitā Notes. https://yesudeep.com/blog/your-application-is-a-graph/ @online{mangalapilly2026your,
author = {Yesudeep Jose Mangalapilly},
title = {Your Application Is a Graph},
journal = {Sa\d{m}hit\=a Notes},
year = {2026},
month = {August},
url = {https://yesudeep.com/blog/your-application-is-a-graph/},
urldate = {2026-08-10},
} Yesudeep Jose Mangalapilly. “Your Application Is a Graph.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/your-application-is-a-graph/. TY - ELEC
AU - Mangalapilly, Yesudeep Jose
TI - Your Application Is a Graph
T2 - Saṃhitā Notes
PY - 2026
UR - https://yesudeep.com/blog/your-application-is-a-graph/
Y2 - 2026-08-10
ER - Dependency injection is often taught as a bag of framework features. This article builds a different model from the ground up: constructor calls become a requirements graph; graph validation becomes a compilation step; build-known selection disappears through specialization; runtime choice remains as a small, explicit branch; and policy admission stays outside wiring. The examples are generic and interactive.
Four lines are enough to assemble a small search screen:
documents → compiled corpus → matcher → search screen
The arrows are the interesting part. The screen needs a matcher. The matcher needs a compiled corpus. The corpus needs documents. Whether those requirements are expressed as constructor parameters, factory arguments, annotations, or a configuration file is secondary.
Martin Fowler's 2004 account of dependency injection described a separate assembler that supplies components with implementations and emphasized the separation of configuration from use. That separation remains the durable idea. A container is one possible assembler; it is not the definition of the problem.
Dependency injection — supplying a component's required collaborators from outside the component, so configuration is separate from use. See Fowler's original account of the name and pattern.
Dependency injection is the act of turning a requirements graph into a concrete application; a container is only one way to perform that act.
Compilation, not lookup
A runtime service locator answers questions such as “give me the implementation registered under this key.” That can be useful for genuinely dynamic systems, but it also means the dependency remains hidden until the line of code that performs the lookup.
The alternative is to validate the requirements graph and then emit or compile ordinary direct wiring. Dagger describes itself as a fully static, compile-time dependency-injection framework. Its annotation processor validates the complete component graph and generates Java source. The generated component ultimately invokes constructors and provider methods; the application does not need to discover each dependency through a string-keyed map on the hot path.
Fruit explores a neighboring point in C++. It uses template metaprogramming to detect most missing bindings and dependency loops at compile time, while retaining an injector and supporting runtime configuration. Guice, one of Fruit's inspirations, is a useful contrast: its injector assembles bindings at runtime and includes runtime bytecode-generation and reflection machinery for some features.
These systems make different tradeoffs. The shared lesson is not “code generation always wins.” It is that a declared graph gives an implementation a chance to reject structural mistakes early and to replace general lookup with direct construction where enough information is known.
A comparison by time of knowledge
The approaches become easier to compare when the columns ask when a fact is available and what remains after that fact is consumed:
| Approach | When the implementation becomes known | What can be rejected then | What remains at runtime |
|---|---|---|---|
| Direct constructor calls | While writing and compiling the application | Ordinary language type errors | The calls themselves |
| Static graph generation, such as Dagger | Build time | Missing bindings, duplicate bindings, and cycles in the generated component graph | Generated direct wiring |
| Compile-heavy injection, such as Fruit | Compilation plus injector configuration | Many graph and type errors during compilation; configuration errors when the injector is assembled | An injector and any deliberately dynamic configuration |
| Runtime container, such as Guice | Injector creation or later lookup | Errors visible to the runtime injector | The container, binding metadata, and requested dynamic machinery |
Compile Select, retain explicit Choose | Each fact's earliest honest boundary | Static graph errors before release; invalid runtime values at the choice boundary | Direct wiring for selected facts and only the explicit finite branches still allowed to vary |
| Open plugin host | Potentially after release | Only the host's manifest, compatibility, signature, and admission checks | Loading, isolation, revocation, and resource-control machinery |
This article advocates the fifth row when variation is finite. It is not a new framework category so much as a discipline: consume a fact at its earliest truthful boundary. A build-known choice should disappear. A user-visible choice should remain explicit. Code supplied after release is a plugin-security problem, not merely a more dynamic injector.
The graph compiler's work is conceptually small:
Lower a validated requirements graph to direct construction.
COMPILE-ROOT(GRAPH, ROOT)
Input: finite attributed GRAPH, requested ROOT
Output: direct construction plan or diagnostics
CHECK-UNIQUE-PROVIDERS(GRAPH)
CHECK-REQUIREMENTS-EXIST(GRAPH)
order ← TOPOLOGICAL-ORDER(REACHABLE-FROM(GRAPH, ROOT))
if order = NIL
return CYCLE-DIAGNOSTIC(GRAPH)
return EMIT-DIRECT-CONSTRUCTION(order, ROOT)For a graph with nodes and dependency arrows, reachability and a topological ordering can both be computed in time proportional to the graph's size, conventionally written . That is build or startup work. The generated application can remain plain constructor calls.
Select what is already known
Suppose the search application has three matcher implementations:
Matcher = Greedy | Nucleo | FZF
There are two importantly different questions:
- Which implementation satisfies the role for this build?
- Which implementation did the running user choose?
Call the first operation Select. The build profile, target platform, or application edition supplies enough information to select one provider. If the choice is Nucleo, the compiler can prune the other branches and emit:
documents → Nucleo corpus → Nucleo matcher → search screen
The running program need not carry a matcher registry or ask which algorithm is active on every query. The decision has already happened.
Partial evaluation — specialize a general program using the inputs already known, producing a smaller residual program for the remaining inputs. See the freely available textbook and examples.
This is ordinary partial evaluation. A general assembler accepts both a graph and a selection. Supplying the selection early produces a specialized assembler—or simply the final direct wiring—for that selection.
Imagine a restaurant prints one dinner menu for Monday and another for Tuesday. Monday's cook does not carry both menus to every table and repeatedly ask which day it is. The day was known when the menu was printed, so Tuesday's dishes are absent. The day is the early fact; printing the smaller menu is specialization; the remaining orders are the facts diners still supply.
Static selection is safe only when the alternatives satisfy the same promised semantics. Two matchers may implement the same method while differing in which results they accept, how they rank ties, or whether extending a query can make a previous match reappear. An interface proves shape, not equivalence. When the difference is visible to the user, choosing between implementations is product behavior and must remain observable.
Choose what must remain dynamic
Call a genuine runtime decision Choose. Unlike Select, Choose is part of the running program's meaning. A setting may let the user pick a matcher. A request may choose a storage region. A tenant may select an approved model provider.
The safest runtime choice is finite and explicit:
choose matcher from {
greedy → construct Greedy branch
nucleo → construct Nucleo branch
fzf → construct FZF branch
}
An unknown value is a configuration error, not the name of arbitrary code to load. Each admitted branch can be validated before release, while only the chosen branch needs to be constructed at runtime.
Timing now matters. There are at least three useful moments for Choose:
- startup: decode one choice, construct one branch, then keep it fixed;
- activation: prepare a replacement branch and publish it for new work; and
- demand: choose separately for each request or operation.
Live activation is not assignment to a global variable. Suppose the old matcher owns a compiled index and two searches are still running when the setting changes. Destroying or mutating it in place gives those searches an ambiguous world. A safer model constructs a new generation:
generation 7: Nucleo index → searches A, B
generation 8: FZF index → searches C, D
Old work may settle into generation 7. It may not publish as if it belonged to generation 8. After its owned work drains, generation 7 can be retired.
Imagine a library replacing its card catalog. Readers already holding cards from the old drawers may finish their trips using those drawers. New readers receive cards from the new catalog. The librarian does not rewrite a card in someone's hand and pretend it always named the new shelf. The catalog edition is the generation; the card is an in-flight operation; finishing before removal is draining.
This is the point where dependency injection touches lifecycle management. A static graph answers “what depends on what.” A live system must additionally answer “which generation owns this value, this operation, and this result?”
The honest limits
Not every application needs a graph compiler. If direct construction fits on one screen, the handwritten assembly is already explicit, fast, and easy to debug. Introducing modules, annotations, scopes, and generated files merely to replace four clear calls is negative value.
Nor is every dependency graph acyclic. Recursive functions, actor systems, event loops, mutually aware UI objects, and feedback controllers may contain cycles in their semantic relationships. Ordinary eager construction cannot resolve a cycle without another mechanism: indirection, laziness, a fixed-point construction, or a redesign that separates creation from communication. A DI tool that simply permits the cycle has not explained its meaning.
Runtime plugin ecosystems also exceed a finite built-in Choose. If strangers may contribute code after release, the problem includes artifact identity, signatures, compatibility, isolation, revocation, capability admission, and resource control. Calling an open plugin registry “dynamic DI” hides the most important half of the design.
Finally, compile-time validation does not make implementations semantically equivalent. It can prove that a matcher supplies the required interface and that its dependencies exist. It cannot infer the product promise that two ranking algorithms return acceptably equivalent results. That requires a domain-owned contract and evidence.
Dos and don'ts
Walk the tree for one real role in an application. Each leaf pairs the smallest honest mechanism with the nearby overreach to avoid.
Whichever leaf wins, four rules still cross the whole tree: ✓ keep providers focused on construction, ✓ put attempts and settlement in producers, ✓ check current authorization at the effect boundary, and ✓ record selection, generation, and effect identity where behavior becomes observable. Conversely: ✕ do not hide effects in constructors, equate constructibility with authority, or trace every trivial getter merely to recover an explanation optimized wiring discarded.
A professional checklist
When a codebase grows beyond direct wiring, ask these questions in order:
- What are the requested roots and their actual requirements?
- Can the requirements be represented as finite inert data?
- Which structural errors can be rejected before execution?
- Which choices are known at build time and should disappear?
- Which choices are genuine startup, activation, or demand behavior?
- Does each live choice construct only its selected branch?
- Which lifetimes and owners label the nodes?
- Are effects separated from pure provision?
- Is authorization rechecked from current facts at the effect boundary?
- Can a result explain its implementation, configuration, generation, and effect identity?
That sequence moves DI from framework taste to an engineering model. It also makes comparisons more precise. Dagger emphasizes static graph validation and source generation. Fruit moves many checks into C++ compilation while retaining runtime injectors and conditional configuration. Guice emphasizes a flexible runtime injector. Handwritten constructors choose the smallest possible system. The right question is not “which framework is most powerful?” It is “at what time is each fact known, and what machinery remains after that fact is used?”
Lessons
- Constructor calls already form a requirements graph. Making it explicit enables missing-edge, cycle, reachability, ordering, lifetime, and ownership checks.
- Static Select and runtime Choose are different operations. Select consumes a build-known fact and can erase alternatives; Choose preserves a finite, observable runtime decision.
- Live choice introduces generations. Prepare, publish, drain, then retire; late old work cannot become new truth.
- An interface proves shape, not semantic equivalence. Product-visible differences must remain named and tested.
- Composition is not authorization. Wiring determines what can exist; policy determines what may happen now; receipts record what happened.
- Providers construct; producers perform effects. Keeping that boundary explicit preserves retry safety, resource bounds, and explanation.
- Direct construction is the baseline. A DI system must justify every piece of machinery it adds over clear handwritten wiring.
Practice
References
- Edsger W. Dijkstra. “Concern for Correctness as a Guiding Principle for Program Composition.” 1970. — EWD 288
- Martin Fowler. “Inversion of Control Containers and the Dependency Injection Pattern.” 2004. — the assembler distinction and separation of configuration from use
- “Basic Usage.” Dagger. — graph bindings, compile-time validation, and generated source
- “Fruit documentation.” Google. — components, compile-time checks, runtime injectors, and rejected features
- “Guice documentation and source.” Google. — runtime injector and binding model
- “Dependencies.” Bazel. — declared and actual dependency graphs
- Neil D. Jones; Carsten K. Gomard; Peter Sestoft. “Partial Evaluation and Automatic Program Generation.” Prentice Hall, 1993. — full text and examples
- Vincent C. Hu et al.. “Guide to Attribute Based Access Control Definition and Considerations.” NIST, 2019. — NIST SP 800-162, updated edition
How to cite
Mangalapilly, Y. J. (2026, August). Your Application Is a Graph. Saṃhitā Notes. https://yesudeep.com/blog/your-application-is-a-graph/ @online{mangalapilly2026your,
author = {Yesudeep Jose Mangalapilly},
title = {Your Application Is a Graph},
journal = {Sa\d{m}hit\=a Notes},
year = {2026},
month = {August},
url = {https://yesudeep.com/blog/your-application-is-a-graph/},
urldate = {2026-08-10},
} Yesudeep Jose Mangalapilly. “Your Application Is a Graph.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/your-application-is-a-graph/. TY - ELEC
AU - Mangalapilly, Yesudeep Jose
TI - Your Application Is a Graph
T2 - Saṃhitā Notes
PY - 2026
UR - https://yesudeep.com/blog/your-application-is-a-graph/
Y2 - 2026-08-10
ER - Webmentions
Annotations
Thank you — your note is held for review and will appear once approved.
Thank you — your note is published.
Please sign in below to leave a note.
