skip to content

How does @DynamicPropertySource interact with Spring's TestContext cache, and what is DynamicPropertyRegistrar? What pitfalls should a principal engineer watch for?

level: principalimportance: nice to knowfreq 26%

answer

  1. TestContext cache key includes dynamic property sources
  2. singleton container + shared base = one cached context
  3. @DirtiesContext evicts cache
  4. DynamicPropertyRegistrar (6.1) = bean-based, can read beans
  5. static method can't see beans

basics

~20 s

Spring caches loaded contexts across test classes to speed suites. Using a shared singleton container plus consistent @DynamicPropertySource keeps the cache key stable so the context is reused. DynamicPropertyRegistrar (Spring 6.1+) lets Spring beans, not just static test methods, contribute dynamic properties.

solid answer

~50 s

The TestContext framework caches ApplicationContexts keyed by their configuration (context config, active profiles, property sources, etc.) so multiple test classes reuse one context. @DynamicPropertySource participates in that configuration, but because it registers a value supplier rather than differing literal properties, tests sharing the same setup still hit the same cache key — important when you use a singleton-container pattern so one Postgres serves the whole suite instead of restarting per class. Pitfalls: (1) per-class containers restart and can fragment the cache and slow tests; (2) leaking @DirtiesContext defeats reuse; (3) suppliers may be invoked multiple times, so keep them pure. DynamicPropertyRegistrar, introduced in Spring Framework 6.1, is a bean-based alternative: you register a DynamicPropertyRegistrar bean (often in a @TestConfiguration) that reads other beans to contribute properties — useful when the dynamic value depends on a Spring bean, which a static @DynamicPropertySource method can't access.

code

java · 20 lines
java
// Singleton-container base: started once, shared context across subclasses
@SpringBootTest
abstract class AbstractPostgresIT {

    static final PostgreSQLContainer<?> POSTGRES =
            new PostgreSQLContainer<>("postgres:16");

    static { POSTGRES.start(); } // started once for the JVM; Ryuk cleans up

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
    }
}

// Subclasses reuse the SAME container AND the SAME cached context
class OrderRepositoryIT extends AbstractPostgresIT { /* @Test ... */ }
class PaymentRepositoryIT extends AbstractPostgresIT { /* @Test ... */ }

go deeper

for a junior

Not expected to know context caching or DynamicPropertyRegistrar.

for a middle

Should at least know the context is cached and reused across classes.

for a senior

Should explain singleton-container sharing and @DirtiesContext eviction.

for a principal

Should reason about cache-key stability, suite performance, and choose DynamicPropertyRegistrar when the value comes from a bean.

## Context caching background Spring's **TestContext framework** caches loaded `ApplicationContext` instances so that test classes with the **same context configuration** share one context instead of paying the (expensive) refresh cost repeatedly. The **cache key** is derived from the merged context configuration: locations/classes, active profiles, `ContextInitializer`s, `@TestPropertySource`, property sources contributed by `@DynamicPropertySource`, `ContextCustomizer`s, parent context, etc. If two test classes produce the same key, they share a context. ### Why this matters with Testcontainers Starting a container is slow. Two common patterns: - **`@Container` (per-class/per-method via `@Testcontainers`)**: container lifecycle tied to the test class → restarts per class. Combined with distinct configs this can create multiple cached contexts. - **Singleton container pattern**: a `static` container started once in a base class (or static initializer) and **never stopped** (Ryuk/JVM shutdown cleans it), with a shared `@DynamicPropertySource`. All test classes extending the base share the **same** container and the **same** context cache entry → fast suites. Because `@DynamicPropertySource` registers **suppliers**, the literal property values don't vary the cache key in a way that breaks reuse when the setup is identical — the key reflects the *presence and identity* of the dynamic source, not a changing port literal. Keep the registering code in a shared base class so the merged config (and thus the cache key) is stable across the subclasses. ## Pitfalls a principal watches for 1. **`@DirtiesContext` overuse** — marks the context dirty and evicts it from the cache, forcing a rebuild and undermining sharing. Use sparingly. 2. **Inconsistent property registration across classes** — if each class registers a slightly different set/order of dynamic properties or different profiles, cache keys diverge → more contexts, more memory, slower runs. 3. **Non-idempotent suppliers** — the value supplier can be invoked more than once and at unpredictable times; it must be side-effect-free (plain getters). 4. **Per-class containers with `@ServiceConnection`/`@DynamicPropertySource`** — fine for isolation, costly for large suites; prefer singleton containers when tests can share state safely. 5. **Eager evaluation bug** — invoking the getter instead of passing a reference (covered earlier) throws when the container isn't up. ## DynamicPropertyRegistrar (Spring Framework 6.1+) `@DynamicPropertySource` is **static** and therefore **cannot read Spring beans** — it only sees static container fields. Sometimes the dynamic value depends on a **bean** in the context (e.g. an embedded server bean whose port is known only after the bean is created). For that, Spring 6.1 introduced the **`DynamicPropertyRegistrar`** functional interface. You expose a `DynamicPropertyRegistrar` **bean**; Spring invokes it during context startup, after beans it depends on are available, letting it contribute dynamic properties from **live beans**. ```java @TestConfiguration class EmbeddedServerConfig { @Bean DynamicPropertyRegistrar serverProps(MyEmbeddedServer server) { return registry -> registry.add("app.server.port", server::getPort); } } ``` Differences vs `@DynamicPropertySource`: - **Bean-based**, so it can depend on and read other beans (via constructor/parameter injection of the bean). - Participates in the normal bean lifecycle rather than the pre-refresh static hook. - Preferred when the value's source is a managed bean, not a static container. ## When to reach for which - Static container value → `@DynamicPropertySource` (or `@ServiceConnection`). - Value derived from a Spring bean → `DynamicPropertyRegistrar` bean. - Supported container, minimal code → `@ServiceConnection`. Getting these right is what keeps a large integration suite both **correct** and **fast** (single shared container + single cached context).

  • Why can't a @DynamicPropertySource method obtain a value from a Spring bean?
    Because it's static and runs before the context refresh, so no beans exist yet; use a DynamicPropertyRegistrar bean instead when the value comes from a bean.
  • How does @DirtiesContext hurt Testcontainers suites?
    It evicts the cached context, forcing a full context rebuild (and, with per-class containers, extra startup), slowing the suite and reducing sharing.

saying these in an interview costs you the question

  • Believing every test class always gets a fresh context regardless of config (ignoring the cache).
  • Thinking @DynamicPropertySource can inject a running bean's value.
  • Assuming a new container must start per test class (missing the singleton pattern).

context