The full picture
interface PaymentProvider { String key(); Receipt charge(Order o); }
@Component class StripeProvider implements PaymentProvider { … }
@Component class AdyenProvider implements PaymentProvider { … }
@Service
class PaymentRouter {
private final Map<String, PaymentProvider> byKey;
PaymentRouter(List<PaymentProvider> providers) { // ALL impls injected
this.byKey = providers.stream()
.collect(toMap(PaymentProvider::key, p -> p));
}
Receipt charge(String provider, Order o) {
return byKey.get(provider).charge(o); // open/closed: new provider
} // = new @Component, zero edits
}
// Type-safe qualifier beats string matching:
@Qualifier @Retention(RUNTIME) @interface Sandbox {}
@Component @Sandbox class SandboxProvider implements PaymentProvider { … }
PaymentRouter(@Sandbox PaymentProvider p) { … }Map<String, T>injection keys by bean name — handy, but a customkey()method survives renames better.@Primary= 'the default';@Qualifier= 'this specific one'; combining them models default-plus-overrides cleanly (e.g., primary real client, qualified test double).ObjectProvider<T>handles optional deps (getIfAvailable), lazy resolution, and streams of candidates without container exceptions.- Profiles/
@ConditionalOn…remove ambiguity at registration time rather than injection time — often the cleanest cut.