Course outline

Spring Boot / Basic: composition and HTTP contracts / Lesson 2

Dependency injection and bean lifecycle

Make collaborators and ownership visible through constructors, then distinguish ordinary Java composition from container discovery, scope, and proxy behavior.

About 5 min readContent reviewed
On this page

Lesson goals

  • Use constructor injection for required collaborators.
  • Resolve bean ambiguity deliberately.
  • Explain singleton scope, lifecycle, and manual construction.

A constructor declares what a service needs

Dependency injection means a component receives collaborators rather than locating or constructing hidden global dependencies itself. A required constructor parameter makes the relationship visible and lets tests supply a controlled implementation. With one constructor, Spring can use it without an extra @Autowired annotation. Validation of the collaborator and validation of a business request are separate responsibilities.

Prefer small capabilities such as Catalog over handing every component the entire ApplicationContext. The latter becomes a service locator that hides the dependency graph. An interface is useful when it represents a meaningful substitution or boundary, not automatically for every class. The container can assemble a design, but it cannot decide whether that design has a sensible direction of dependencies.

Keep the framework boundary thin

The service fragment declares Catalog as a required collaborator and performs a bounded domain calculation. It is not a complete Boot project: the Catalog contract and a managed implementation must exist, and discovery must include the service. The fragment remains non-runnable because container creation and bean discovery were not executed. Its method should not contain HTTP status or datasource setup merely because it is managed.

A controller can adapt a request to this service and map its result later. Keeping policy outside transport code permits direct tests without a listener. Do not return every persistence entity directly through this boundary; a deliberate result model can avoid coupling the public API to lazy relationships or internal fields. Composition and API shape remain explicit choices.

java An illustrative constructor-injected service
package training.catalog;

import org.springframework.stereotype.Service;

@Service
public class QuoteService {
    private final Catalog catalog;
    public QuoteService(Catalog catalog) { this.catalog = catalog; }
    public int total(String code, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("quantity");
        return Math.multiplyExact(catalog.unitPrice(code), quantity);
    }
}

Catalog is a contextual application capability. The fragment shows composition intent, not observed bean discovery or a launched application.

Verify domain composition independently of Spring

The complete JDK-only program assembles a service with a synthetic catalog and calculates two units at ten each. It prints20. This validates ordinary constructor composition and calculation, not Spring annotations, proxies, component scanning, or transactions. Labeling the model's limited evidence prevents a passing unit calculation from being misreported as framework integration coverage.

A production capability may throw an operational failure or report a missing item. Tests should distinguish those from a valid zero price rather than making every fake return a convenient value. Keep fake behavior aligned with the real contract, and retain separate adapter tests for serialization, authorization, and persistence. A stub is not evidence that an external system will behave identically.

java Dependency-free composition contract model
public class Main {
interface Catalog { int unitPrice(String code); }
static final class Quote {
 private final Catalog catalog;
 Quote(Catalog catalog) { this.catalog=java.util.Objects.requireNonNull(catalog); }
 int total(String code,int quantity) {
  if(quantity<=0) throw new IllegalArgumentException("quantity");
  return Math.multiplyExact(catalog.unitPrice(code),quantity);
 }
}
 public static void main(String[] args) throws Exception {
Catalog catalog=code->10;
Quote quote=new Quote(catalog);
System.out.println(quote.total("BOOK",2));
 }
}
Expected output
20

The synthetic capability returns10 and the service returns20. No Spring container or external service is involved.

A singleton is not automatic thread safety

Spring's default singleton scope is one bean instance per container definition, not one new object per HTTP request. Mutable request-specific fields can therefore leak across concurrent callers. Prefer immutable collaborators and local request state, or a clearly justified scope and synchronization design. final on a collaborator reference does not freeze the object it refers to.

Calling new bypasses container-managed lifecycle and any proxy-based behavior expected for that object. Conversely, a proxy is not the underlying service type in every reflection scenario. When using @Configuration(proxyBeanMethods=false), prefer bean-method parameters for dependencies rather than assuming direct method calls will always return container-managed singleton instances. Understand the selected configuration semantics before optimizing annotation syntax.

Repair the dependency graph instead of hiding it

A missing bean, several matching candidates, and a constructor cycle are different failures. Inspect the exact required type, active configuration, package discovery, and candidate qualifiers. Use an explicit qualifier or primary choice when the domain really has alternatives. A cycle often indicates responsibilities that should be separated rather than a need to enable circular references globally.

Test the composition root with the narrowest suitable context and verify lifecycle cleanup for owned resources. Do not place slow remote initialization in constructors to make creation appear complete. A well-defined graph states who creates, owns, replaces, and closes each collaborator; that clarity is the benefit of dependency injection, beyond avoiding the spelling of new in application code.

Common errors and fixes

NoSuchBeanDefinitionException

Why it happens
No managed candidate satisfies the required dependency.
How to fix it
Inspect scanning, imports, profiles, and the exact type.
Prevent it next time
Test the intended composition root.

NoUniqueBeanDefinitionException

Why it happens
Several candidates match without a deliberate selection rule.
How to fix it
Use an appropriate qualifier or primary policy.
Prevent it next time
Document meaningful implementations and selection.

BeanCurrentlyInCreationException

Why it happens
A creation cycle prevents the declared construction graph.
How to fix it
Separate responsibilities or introduce a reviewed boundary.
Prevent it next time
Keep constructor dependencies acyclic where possible.

Check your understanding

1. Why can storing the current customer in a singleton service field leak data?

Show worked solution

The same managed instance can serve multiple concurrent requests. One caller can overwrite another caller's state. Keep request data in method parameters/local values or a deliberate request-scoped contract, and enforce authorization independently rather than assuming object scope isolates tenants.

2. What does the passing plain-Java quote model prove and not prove?

Show worked solution

It proves the shown calculation and constructor relationship for its synthetic input. It does not prove scanning, bean selection, proxy advice, HTTP binding, database transactions, or production catalog behavior. Add scoped framework and adapter tests for those independent boundaries.

Key takeaways

  • Required constructors expose the dependency contract.
  • Singleton scope and proxy behavior need explicit reasoning.
  • Domain unit evidence and container integration evidence are different.

Official references