If a JUnit 5 extension puts something that must be shut down — an open connection pool or a started server — into the ExtensionContext Store, how does it get closed, and when exactly does that happen?
answer
- CloseableResource marker → close() on context close
- 5.13+ also closes plain AutoCloseable (CloseableResource deprecated)
- Scope of store = lifetime: method / class / root(run)
- Reverse insertion order (LIFO) teardown
- remove() does NOT close; hard kill closes nothing
basics
~20 sStore the value as an ExtensionContext.Store.CloseableResource (recent JUnit 5 versions also auto-close plain AutoCloseable values). When the context that owns the store is closed — end of the test method, class, or whole run for the root context — JUnit calls close() on those values in reverse insertion order.
solid answer
~50 sThe store owns cleanup, so you rarely write teardown code by hand. Store a value that implements `ExtensionContext.Store.CloseableResource` (the classic 5.x mechanism); in recent JUnit 5 releases plain `AutoCloseable` values are closed too and `CloseableResource` is deprecated in favour of it. When the **owning context closes**, JUnit invokes `close()` on each such value in **reverse insertion order** — LIFO, so things are torn down in the opposite order they were built. Timing follows the context you chose: - method-level store → closed after that test finishes; - class-level store → after the class's tests finish, i.e. around `@AfterAll`; - `getRoot()` store → at the very end of the test run. So the standard "one container for the whole suite, shut down at the end" pattern is: `getRoot().getStore(ns).getOrComputeIfAbsent(key, k -> startAndWrapCloseable(), ...)`. Two caveats: `remove()` takes a value out **without** closing it, and a JVM crash or hard kill closes nothing — external resources still need their own reaper.
code
java · 17 linesclass DbExtension implements BeforeAllCallback {
private static final Namespace NS = Namespace.create(DbExtension.class);
@Override
public void beforeAll(ExtensionContext ctx) {
ctx.getRoot().getStore(NS)
.getOrComputeIfAbsent("db", k -> new SharedDatabase(), SharedDatabase.class);
}
static class SharedDatabase implements ExtensionContext.Store.CloseableResource {
private final PostgreSQLContainer<?> container = new PostgreSQLContainer<>("postgres:16");
SharedDatabase() { container.start(); }
String jdbcUrl() { return container.getJdbcUrl(); }
@Override public void close() { container.stop(); }
}
}go deeper
Know that a value in the store can be closed automatically when the scope ends, so you rarely write manual teardown.
Name the CloseableResource contract (and the newer AutoCloseable support), and map store scope to close timing: method, class, root/run.
Add LIFO ordering, the remove()-does-not-close trap, and why external reapers are still needed for hard failures.
Weigh run-wide shared fixtures against isolation and parallelism, and decide where cleanup ownership sits — framework store, build tooling, or infrastructure reaper.
## Why the store handles cleanup An extension that starts something — a Testcontainers instance, an embedded broker, a headless browser, a temporary directory — must stop it, or CI leaks processes until it runs out of resources. Doing that in an `afterAll` callback works only when the scope matches the extension's callbacks exactly. The store generalises it: the resource is cleaned up when **the scope it was stored in** ends, whatever that scope is. ## The mechanism Historically (JUnit 5.1 onwards) the marker is the nested interface `ExtensionContext.Store.CloseableResource`, with a single `close()` method. Any value put into a store that implements it is closed automatically when the owning `ExtensionContext` is closed. ```java class Browser implements ExtensionContext.Store.CloseableResource { final WebDriver driver = new ChromeDriver(); @Override public void close() { driver.quit(); } } ``` In recent JUnit 5 releases (5.13+) the store additionally closes values that merely implement `java.lang.AutoCloseable`, and `CloseableResource` is deprecated in favour of that — which removes the need to wrap ordinary closeable objects. If you target older 5.x versions, keep implementing `CloseableResource`; if your build is on a current version, plain `AutoCloseable` is the simpler path. Check your JUnit version before relying on either behaviour. ## When close() runs A context is closed when its node finishes executing, so the scope you picked when calling `getStore` is exactly the lifetime you get: - Stored on the **method** context → closed when that test method completes (after its after-each callbacks). - Stored on the **class** context → closed when the class finishes, effectively after `@AfterAll`. - Stored on the **root** context (`context.getRoot()`) → closed at the end of the whole test run, after every class in that launcher execution. Within one store, values are closed in **reverse insertion order**. That matters when resources depend on each other: a data source inserted after the container it connects to is closed before the container is stopped, which is what you want. Exceptions thrown from `close()` are reported and do not silently vanish, but they surface as run-level failures rather than test failures — so a noisy shutdown can turn a green run red. ## The one-container-per-run pattern ```java Database db = context.getRoot() .getStore(Namespace.create(DbExtension.class)) .getOrComputeIfAbsent("db", k -> new Database(), Database.class); ``` `getOrComputeIfAbsent` guarantees a single creation even under parallel execution, the root store makes it live for the run, and `Database` implementing the closeable contract makes the shutdown automatic. This is the canonical alternative to a static field plus a shutdown hook — and it is better, because the framework, not the JVM's exit path, controls the ordering. ## Traps 1. **`remove()` does not close.** Taking a value out of the store hands you ownership; you must close it yourself. 2. **Wrong scope, wrong lifetime.** Storing a per-run resource in the method context restarts it for every test and quietly kills performance; storing per-test state at root leaks it across the run. 3. **Nothing runs on a hard kill.** `close()` needs an orderly end of the run; a killed CI job, an OOM, or `System.exit` skips it. Long-lived external resources still need an independent reaper (Testcontainers' Ryuk exists exactly for this). 4. **Reused JVMs.** With a forked, JVM-reusing build the root context still closes at the end of each launcher execution, so do not assume "root" means "process lifetime". 5. **Don't double-manage.** If the resource is also closed by an `afterAll` callback you wrote, you will close it twice; make `close()` idempotent or pick one owner.
- In what order are multiple closeable values in one store closed, and why does the order matter?In reverse insertion order — the last value stored is closed first. It matters when resources depend on one another: a connection pool created after the database it points at is closed before the database stops, so no component is torn down while something still uses it. It is the same discipline as nested try-with-resources.
- Does calling remove(key) on the store close the value?No. remove() simply takes the entry out of the store and returns it, transferring ownership to the caller; nothing is closed. If you remove a resource you must close it yourself, otherwise it leaks for the rest of the run because the store no longer knows about it.
- Your CI job is killed mid-run and containers are left running. Is the store contract broken?No — automatic closing depends on the run finishing in an orderly way so that contexts are closed. A SIGKILL, an OOM-killed JVM, or System.exit bypasses it entirely. External resources that must not leak need an independent reaper such as Testcontainers' Ryuk, or CI-level cleanup, in addition to store-based cleanup.
The store is a shipping manifest: everything loaded into a given crate is unpacked when that crate is opened at its destination, last item first. Take an item off the manifest by hand and nobody unpacks it for you.
saying these in an interview costs you the question
- Expecting a plain object with a close() method to be closed automatically on older JUnit versions without implementing CloseableResource
- Thinking remove() closes the value
- Assuming root-store resources are closed even when the JVM is killed
- Believing values close in insertion order rather than reverse
- Storing a per-run resource in the method context and wondering why it restarts for every test