skip to content

How does @ServiceConnection work internally — what turns a @Container into wired beans?

level: seniorimportance: should knowfreq 30%

answer

  1. ContextCustomizer scans @ServiceConnection
  2. ContainerConnectionSource per container
  3. ConnectionDetailsFactory SPI matches type/name
  4. Registers typed ConnectionDetails bean
  5. Auto-config prefers ConnectionDetails; part of context cache key

basics

~10 s

Boot scans the test class for @ServiceConnection fields, builds a ConnectionSource for each container, and uses a matching ConnectionDetailsFactory to create a typed ConnectionDetails bean. Auto-configuration then consumes that bean instead of properties.

solid answer

~40 s

During test context setup, Boot's ServiceConnection support (a ContextCustomizer contributed for the test) discovers fields and bean methods annotated @ServiceConnection. For each it builds a ContainerConnectionSource capturing the container, image, and any name hint. It then consults the ConnectionDetailsFactory SPI: each factory declares the service it handles and, when it matches (by container type or by name), produces a typed ConnectionDetails — e.g. JdbcConnectionDetails, RedisConnectionDetails, KafkaConnectionDetails. Boot registers that ConnectionDetails as a bean in the ApplicationContext. Auto-configuration classes like DataSourceAutoConfiguration are written with @ConditionalOnBean-style logic that prefers a ConnectionDetails bean over spring.* properties, so the DataSource/Redis client/etc. is built from the container's real host, mapped port, and credentials. The container must be running when details are read, which the @Testcontainers extension or bean lifecycle guarantees.

code

java · 16 lines
java
// Sketch of a custom factory for a bespoke service so
// @ServiceConnection(name = "my-cache") works on a GenericContainer.
class MyCacheConnectionDetailsFactory
    extends ContainerConnectionDetailsFactory<Container<?>, MyCacheConnectionDetails> {

    MyCacheConnectionDetailsFactory() {
        super("my-cache"); // matches @ServiceConnection(name = "my-cache")
    }

    @Override
    protected MyCacheConnectionDetails getContainerConnectionDetails(
            ContainerConnectionSource<Container<?>> source) {
        return new MyCacheConnectionDetailsImpl(source);
    }
}
// Register via META-INF/spring/...ConnectionDetailsFactory.imports (auto-config metadata).

go deeper

for a junior

Not expected to know internals beyond 'Boot reads the container and wires beans'.

for a middle

Should know a ConnectionDetails bean is produced and consumed by auto-config.

for a senior

Should describe the ContextCustomizer discovery, ConnectionDetailsFactory matching, and precedence over properties.

for a principal

Should reason about the context-cache-key impact, lifecycle ordering, and writing/registering a custom ConnectionDetailsFactory.

## The pipeline, step by step ### 1. Discovery via a ContextCustomizer Spring's `TestContext` framework lets libraries contribute a **`ContextCustomizer`**. Spring Boot registers one that scans the test class for `@ServiceConnection`-annotated **static `@Container` fields** and **container `@Bean` methods**. This runs while the `ApplicationContext` for the test is being prepared (and the customizer participates in the **context cache key**, so different service-connection setups get different cached contexts). ### 2. Building a ConnectionSource For each annotated element Boot creates a **`ContainerConnectionSource`** — an abstraction holding the container instance, its Docker image name, the optional `name` hint from the annotation, and origin info for error messages. ### 3. Matching a ConnectionDetailsFactory Boot loads all **`ConnectionDetailsFactory`** implementations registered through its auto-configuration metadata (the SPI). Each factory (e.g. a Postgres JDBC factory) inspects the `ContainerConnectionSource` and decides whether it applies — matching on the **container's Java type** (e.g. `JdbcDatabaseContainer`/`PostgreSQLContainer`) or on the supplied **service name**. The first applicable factory produces a concrete **`ConnectionDetails`** implementation. `ConnectionDetails` subtypes are typed views of a service's coordinates: `JdbcConnectionDetails.getJdbcUrl()/getUsername()/getPassword()`, `RedisConnectionDetails.getStandalone()`, `KafkaConnectionDetails.getBootstrapServers()`, etc. Ports read from the container are the **mapped host ports**, so they reflect the actual runtime binding. ### 4. Registering the bean Boot registers the produced `ConnectionDetails` as a bean in the context. ### 5. Auto-config consumes it Boot's auto-configuration was refactored in 3.1 around `ConnectionDetails`. Classes like `DataSourceAutoConfiguration`, `RedisAutoConfiguration`, `KafkaAutoConfiguration` first look for the relevant `ConnectionDetails` bean and use it; only if absent do they fall back to `spring.datasource.*` / `spring.data.redis.*` / `spring.kafka.*` properties. That's the precedence rule you can observe from the outside. ## Lifecycle ordering — why timing matters The connection details (URL, port) are only valid once the container is **started**. Two cases: - **@Container field style**: the JUnit 5 `@Testcontainers` extension starts the container. A `static` field starts once per class before the context is used. - **@Bean container style**: Spring Boot's `TestcontainersLifecycleApplicationContextInitializer` detects `Startable` container beans and starts them as part of context refresh, stopping them on close. Either way, the container is up before its `ConnectionDetails` are read/consumed. ## Extensibility You can implement your own `ConnectionDetailsFactory<ContainerConnectionSource<C>, D>` and register it (Boot's `spring.factories`/auto-config metadata) to support a service Boot doesn't ship, so `@ServiceConnection(name = "my-service")` works for a bespoke container. ## Gotchas - Because the customizer is part of the **context cache key**, changing service-connection setup can cause a new context to be built (affects test suite speed). - If **no factory matches**, no `ConnectionDetails` bean is created — beans silently fall back to properties/defaults, which usually fails to connect; a bad explicit `name` fails fast instead. - Reading details from a **stopped** container yields errors — normally prevented by the lifecycle above, but relevant if you manage containers manually.

  • Why can changing @ServiceConnection setup slow a test suite down?
    The ServiceConnection ContextCustomizer participates in the context cache key, so a different setup produces a different cached ApplicationContext, causing an extra context to be built and cached.
  • How would you support a service Spring Boot has no factory for?
    Implement a ConnectionDetailsFactory (typically extending ContainerConnectionDetailsFactory) that matches by name or type, register it via Boot's auto-config metadata, then use @ServiceConnection(name = ...).

saying these in an interview costs you the question

  • Saying it works by setting system properties reflectively (it registers typed ConnectionDetails beans).
  • Claiming there's no extension point (custom ConnectionDetailsFactory exists).
  • Ignoring that details are only valid after the container starts.

context