You're viewing the readable version of this site. The interactive extras (search, diagrams, read-aloud) need JavaScript and a current browser. Enable JavaScript; if it is already enabled, update your browser.

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.

· · 15 min read

dependency-injection, software-architecture, graphs, partial-evaluation, code-generation, authorization

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
APA
Mangalapilly, Y. J. (2026, August). Your Application Is a Graph. Saṃhitā Notes. https://yesudeep.com/blog/your-application-is-a-graph/
BibTeX
@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},
}
Plain
Yesudeep Jose Mangalapilly. “Your Application Is a Graph.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/your-application-is-a-graph/.
RIS
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.

The graph hidden in the constructors

Start with ordinary direct construction. No framework is required:

Assemble one search screen from its requirements.

ASSEMBLE-SEARCH(DOCUMENTS)
Input:  finite document collection DOCUMENTS
Output: ready search screen

corpus  COMPILE-CORPUS(DOCUMENTS)
matcher  MAKE-MATCHER(corpus)
screen  MAKE-SEARCH-SCREEN(matcher)
return screen

Reading from bottom to top reveals the requirements. Reading from top to bottom reveals a valid construction order. We can write the same information without the calls:

search-screen: matcher
matcher: corpus
corpus: documents
documents

Each name is a node. A line after the colon names the nodes it requires. The requested result—here, search-screen—is the root.

Directed acyclic graph (DAG) — a set of nodes connected by one-way arrows, with no route that eventually returns to its starting node. Acyclicity permits an ordering in which every requirement is ready before its consumer. See Bazel's dependency-graph explanation.

This is the same broad shape used by a build system, but the labels mean different things. Bazel says that one target needs another artifact at build or execution time; its documentation calls the resulting relation a DAG. An application composition graph says that one value needs another value to be constructed. Bazel produces artifacts. The assembler produces an object graph. The mathematics travels; the meaning of an edge does not.

Before using the analyzer, predict what should happen when documents changes.

Prediction checkpoint. Identify the exact downstream consequence of changing the root data source.

Now edit the graph. Click a node to see precisely which constructed values depend on it. Add a missing name or a cycle and the analyzer refuses the graph before any construction begins.

A live requirements graph. Edit the finite step language, analyze it, and click a node to reveal its downstream reconstruction cone.

Making this graph explicit buys several checks before the application starts:

  • every requested dependency has one admissible provider;
  • ordinary construction contains no cycle;
  • only nodes reachable from the requested roots need to exist;
  • every dependency can be constructed before its consumer; and
  • a short-lived value is not captured by an object intended to outlive it.

The last item shows why the graph needs labels. A node is not merely “a service.” It has a type, lifetime, owner, and perhaps capabilities. A database connection that belongs to one request must not quietly become a process-wide singleton because both happen to implement the same interface.

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)
Graph compilation. Put the graph-to-program stages in the order that preserves early diagnostics and removes unreachable work.

For a graph with |V||V| nodes and |E||E| dependency arrows, reachability and a topological ordering can both be computed in time proportional to the graph's size, conventionally written O(|V|+|E|)O(|V| + |E|). 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:

  1. Which implementation satisfies the role for this build?
  2. 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. Predict what remains after a build-known implementation has been specialized.

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.

Runtime activation. Put a live implementation change in the order that preserves in-flight work.

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

Wiring is not authorization

Suppose an application can construct both a local model and a cloud model. The composition graph may prove that the cloud branch has a network client, a credential source, and a response decoder. None of that means the current document may leave the machine.

Composition and policy answer different questions:

composition: can this implementation be constructed?
policy:      may this actor perform this operation on this resource now?
receipt:     what effect actually happened?

The distinction is temporal. Build-time validation can bound possible authority: the cloud provider declares that it may require one credential and network access to one host. Runtime policy evaluates current facts: the actor, resource, requested operation, environment, delegation, confidentiality, and expiry.

NIST SP 800-162 defines attribute-based access control in this shape: authorization evaluates attributes of the subject, object, requested operation, and sometimes the environment against policy. A simple access-control list is one restricted case. It is often insufficient for choices whose legality depends on data classification, current consent, budget, or delegation.

Policy admission — a fresh decision about whether a particular actor may perform a particular operation on a particular resource under current facts. For a standard attribute-based model, see NIST SP 800-162.

Authority boundary. Decide whether successful construction permits a cloud effect.

A robust effect path therefore checks twice for different reasons. The graph compiler rejects a provider whose declared capability needs exceed the application's envelope. Immediately before an effect, policy revalidates the fresh grant. Authority may have changed since startup; injection must never mean “authorized forever.”

Providers are not producers

One final separation keeps the model honest. Constructing a value and performing an effect are not the same operation.

provider: configuration → API client
producer: request + API client → external attempt → observed result

The provider graph should be as pure as practical: validate configuration and construct values. The producer graph owns retries, cancellation, deadlines, idempotency, resource budgets, and settlement. A constructor that quietly sends a request prevents the composition compiler from knowing whether construction is repeatable and prevents the execution layer from attributing the external attempt.

This distinction also answers a debugging objection. A direct generated call path is cheap, but optimized wiring can erase the human-visible story of how it was selected. Preserve that story as structured data at the boundaries that matter:

requested role
selected implementation
configuration identity
activation generation
effect and settlement identity

Tracing every getter would impose cost on the simplest provisions. Recording selection once per build or activation, and recording effect identity at the producer boundary, preserves explanation where behavior becomes observable.

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.

Proportional machinery. Decide when an explicit DI compiler would make a small program harder rather than clearer.

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.

The mechanism test. Answer according to when the implementation may become known and whether live work must survive replacement.

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:

  1. What are the requested roots and their actual requirements?
  2. Can the requirements be represented as finite inert data?
  3. Which structural errors can be rejected before execution?
  4. Which choices are known at build time and should disappear?
  5. Which choices are genuine startup, activation, or demand behavior?
  6. Does each live choice construct only its selected branch?
  7. Which lifetimes and owners label the nodes?
  8. Are effects separated from pure provision?
  9. Is authorization rechecked from current facts at the effect boundary?
  10. 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

Retrieval. Reconstruct the central model without relying on framework vocabulary.
Discrimination. Decide whether an implementation decision is Select or Choose.
Transfer. Apply the model to a storage migration not discussed in the article.

References

  1. Edsger W. Dijkstra. “Concern for Correctness as a Guiding Principle for Program Composition.” 1970. — EWD 288
  2. Martin Fowler. “Inversion of Control Containers and the Dependency Injection Pattern.” 2004. — the assembler distinction and separation of configuration from use
  3. Basic Usage.” Dagger. — graph bindings, compile-time validation, and generated source
  4. Fruit documentation.” Google. — components, compile-time checks, runtime injectors, and rejected features
  5. Guice documentation and source.” Google. — runtime injector and binding model
  6. Dependencies.” Bazel. — declared and actual dependency graphs
  7. Neil D. Jones; Carsten K. Gomard; Peter Sestoft. “Partial Evaluation and Automatic Program Generation.” Prentice Hall, 1993. — full text and examples
  8. 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

APA
Mangalapilly, Y. J. (2026, August). Your Application Is a Graph. Saṃhitā Notes. https://yesudeep.com/blog/your-application-is-a-graph/
BibTeX
@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},
}
Plain
Yesudeep Jose Mangalapilly. “Your Application Is a Graph.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/your-application-is-a-graph/.
RIS
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  - 

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.