ADR-0006: Disambiguate Spring beans by concrete type, not field @Qualifier

  • Status: Accepted
  • Date: 2026-05-31
  • Scope: core (any Spring backend using Lombok constructors)

Context

When several beans are assignable to the same interface (e.g. @EnableAsync +
scheduling expose two TaskExecutor-assignable beans), an injected dependency
needs disambiguation. The reflex is a field-level @Qualifier on a
@RequiredArgsConstructor class — but Lombok generates the constructor, and a
field @Qualifier is only carried onto the generated constructor parameter if a
lombok.config declares it under lombok.copyableAnnotations. These backends
keep no lombok.config
(and in Expenses the Docker build copies only
mvnw/pom.xml/.mvn/src anyway), so a field @Qualifier on a Lombok
constructor is silently not honored → Spring sees an ambiguous bean and fails
to start.

Decision

Disambiguate by injecting the concrete type, not a field @Qualifier, on
@RequiredArgsConstructor classes. Inject the bean’s concrete class (which is
unambiguous by type) rather than the shared interface + a qualifier.

Example (Expenses: presentation/chat/webservice/ChatWebService.java): inject the
concrete ThreadPoolTaskExecutor applicationTaskExecutor, not
@Qualifier("applicationTaskExecutor") TaskExecutor — only the former survives
Lombok’s generated constructor here.

If a future need genuinely requires field-level qualifiers on Lombok constructors,
that requires adding a lombok.config with copyableAnnotations and ensuring
it’s copied into the Docker image — neither of which is true today, so don’t rely
on it.

Consequences

  • Bean wiring is unambiguous without depending on a lombok.config we don’t
    maintain; avoids an ambiguous-bean startup failure.
  • Slightly less flexible than qualifiers, but type-based injection is clearer anyway.
  • Build context: the backend Dockerfile (copies mvnw/pom.xml/.mvn/src only)
  • Operational detail: the backend repo’s CLAUDE.md