Inversion of Control is one of the most impactful ideas in software engineering. It fundamentally changed how we structure applications — making code testable, modular, and composable. Dependency Injection, its most common implementation, became the backbone of virtually every modern Java framework.
But somewhere along the way, DI acquired a second job. And that second job is quietly causing problems.
Assembly vs. Provisioning
DI was designed to solve one problem: assembling an application from its components. Controller depends on service, service depends on repository — wire them together, done. The components are internal. The wiring is deterministic. Configuration is minimal or zero.
But most frameworks also use DI for something fundamentally different: resource provisioning. Database connections, HTTP clients, message brokers, caches, stream consumers — external resources that the application needs access to.
These two concerns look similar on the surface. Both involve "giving a component something it needs." But they behave completely differently:
Assembly — internal components, part of the application. Minimal or no configuration. Created once at startup. Dependencies are your code. No security concerns. Failures are deterministic, caught at compile time.
Provisioning — external resources, part of the infrastructure. Environment-dependent, complex configuration. Managed lifecycle: pools, reconnects, health checks. Dependencies are drivers, SDKs, adapters. Requires credentials, certificates, rotation. Failures are environmental, discovered at runtime.
By fusing these into one mechanism, frameworks created a set of consequences that the industry accepted as normal.
The Consequences
Your application bundles infrastructure.
Database drivers, connection pools, HTTP client libraries, messaging SDKs — they all live in your application's dependency tree. A typical backend service is 60% infrastructure dependencies, 40% business logic. Your pom.xml is a manifest of things that aren't your problem but became your problem.
Environment leaks into the artifact.
Dev, staging, production — each needs different resource configurations. Connection strings, pool sizes, timeouts, retry policies. The application artifact is identical, but the resource configuration isn't. Managing these variations across services and environments becomes a combinatorial challenge that grows with every new service and every new resource.
Credentials live where they shouldn't.
Because the application provisions its own resources, it needs secrets. Database passwords, API keys, certificates. Every service, every environment. Now multiply by the number of services. Secret management becomes an infrastructure project of its own — not because the problem is inherently hard, but because every application was given responsibility for its own credentials.
Infrastructure changes become application changes.
Upgrading a connection pool library? Touch every service. Switching to a different database driver? Rebuild and redeploy every service that uses a database. A security CVE in an adapter library requires coordinated redeployment across the fleet. Infrastructure concerns create application-level change propagation.
Testing fights the wrong battle.
A significant portion of test setup is dedicated to mocking or configuring infrastructure that isn't the application's concern. Integration tests need containers for databases, message brokers, caches. The test is verifying business logic, but the setup is provisioning resources.
Separating the Concerns
What if assembly and provisioning were handled by different mechanisms, at different layers?
Assembly — connecting internal components — can be fully automated. If a use case depends on a repository, and both are part of the application, the wiring is deterministic. No configuration file needed. Annotation processing can resolve it at compile time.
Provisioning — providing access to external resources — belongs to the runtime environment, not the application. The application declares what it needs: "I need a SQL connection," "I need a notification channel," "I need a stream." The runtime decides how to provide it based on the deployment environment.
This separation has immediate consequences:
-
pom.xmlcontains only business dependencies. Infrastructure dependencies belong to the runtime. - Same artifact deploys to any environment without configuration changes.
- Credentials never touch the application. The runtime handles authentication with the infrastructure.
- A security patch in a database driver is a runtime update — zero application rebuilds, zero redeployments.
- Tests focus on business logic. No mock infrastructure, no test containers for resources the application doesn't own.
The application becomes what it should have been from the start: pure business logic with declared resource requirements. Everything between the business logic and production is the runtime's responsibility.
The Boundary
This isn't an argument against DI. Inversion of Control remains one of the best ideas our field has produced. The argument is against overloading DI with a concern it wasn't designed for — and paying the price in dependency sprawl, configuration complexity, and operational coupling.
The boundary is clear: if it's your code, assemble it. If it's infrastructure, declare it and let the environment provide it.
This is the fifth article in the "We Should Write Java Code Differently" series. Previous: Frictionless Prod.
Top comments (0)