skip to content

In JUnit 5, what does the @ResourceLock annotation do, and when would you reach for it?

level: middleimportance: must knowfreq 34%

answer

  1. named key + READ / READ_WRITE
  2. lock covers the node, its subtree and its callbacks
  3. keys are plain strings — a typo silently disables it
  4. Resources.SYSTEM_PROPERTIES, TIME_ZONE, LOCALE
  5. cheaper than serialising the whole suite

basics

~20 s

@ResourceLock declares that a test class or method needs a named shared resource. When tests run in parallel, JUnit will not let two tests hold conflicting locks on the same key at once, so tests that touch the same shared state are serialised while everything else keeps running concurrently.

solid answer

~50 s

`@ResourceLock` is JUnit 5's synchronisation primitive for parallel test execution. You put it on a test class or method with a **key** — an arbitrary string naming some shared resource — and an access **mode** of `READ` or `READ_WRITE`. Before executing the annotated node the engine acquires a lock on that key and holds it until the node and everything nested inside it finishes, including its `@BeforeEach`/`@AfterEach` callbacks. The key is a pure convention: JUnit does not know what `"app.database"` means. Two tests coordinate only because they spell the key identically. `org.junit.jupiter.api.parallel.Resources` supplies constants for the well-known JVM-global ones — `SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, `TIME_ZONE` — so everyone uses the same string. You reach for it when the shared state genuinely cannot be removed: a JVM-wide setting, a single file, a fixed port, a singleton registry. It is a targeted alternative to serialising a whole class, since unrelated tests keep running in parallel.

code

java · 23 lines
java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;

class FeatureFlagTest {

    @Test
    @ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
    void enablesFlagViaSystemProperty() {
        System.setProperty("feature.newCheckout", "true");
        try {
            assertTrue(FeatureFlags.isEnabled("newCheckout"));
        } finally {
            System.clearProperty("feature.newCheckout");
        }
    }

    @Test
    void parsesFlagName() { // no lock: touches nothing shared
        assertEquals("newCheckout", FeatureFlags.normalise(" NewCheckout "));
    }
}

go deeper

for a junior

Say it names a shared resource so JUnit serialises tests that use it, and that READ_WRITE is exclusive.

for a middle

Explain that keys are plain strings agreed by convention, that the lock spans the node's callbacks and subtree, and that Resources supplies canonical constants.

for a senior

Contrast it with execution mode and whole-suite isolation, argue for method-level locks and short critical sections, and treat each lock as recorded shared-state debt.

for a principal

Discuss it as a suite-wide concurrency policy: who owns the key vocabulary, when locking is the wrong answer versus redesigning tests, and the throughput cost of a hot key.

## The problem it solves With parallel execution enabled, any two test methods may be in flight at the same time. That is fine while tests are independent, but some state cannot be duplicated per test: a system property, the JVM default time zone, `System.out`, a file at a fixed path, a TCP port, a singleton cache, a row in a shared database. If one test mutates such a thing while another reads it, results become non-deterministic. There are two honest fixes. The first and better one is to eliminate the sharing — inject the dependency instead of mutating a global, use a per-test temporary directory, bind an ephemeral port. The second, for cases where the shared thing is genuinely global, is to tell JUnit about it so it can serialise exactly the tests that conflict. `@ResourceLock` is that second mechanism. ## Anatomy ```java @ResourceLock(value = "app.config.file", mode = ResourceAccessMode.READ_WRITE) ``` - **value** — the resource key. Any string. It is an identifier, not a path or a real handle; JUnit never touches the thing it names. - **mode** — `ResourceAccessMode.READ` or `READ_WRITE` (the default). `READ_WRITE` is exclusive; `READ` is shared with other `READ` holders of the same key. The annotation is repeatable — declare several and a node needing more than one resource acquires all of them. It can be placed on a test class, a `@Nested` class, or an individual test method. ## Scope and lifetime of the lock The lock is acquired **before** the annotated node starts and released **after** it completes, and it covers the node's entire subtree and lifecycle callbacks. Two consequences follow: 1. On a test method, the lock covers `@BeforeEach`, the test body and `@AfterEach`. That matters because the risky mutation is frequently in setup or teardown, not in the test body; a lock that only covered the body would leave the race in place. 2. On a class, the lock covers every method in that class — and since the annotated node's descendants are put on a single thread, the methods of a lock-annotated class do not run concurrently with each other either. Locking at class level is therefore a heavier hammer than it looks; prefer method-level locks when only some methods touch the resource. ## The key is a convention, not a discovery The most common beginner error is expecting magic: writing `@ResourceLock("database")` on one test and `@ResourceLock("db")` on another and wondering why they still collide. JUnit compares strings. Nothing enforces that the key corresponds to anything real. That is why `org.junit.jupiter.api.parallel.Resources` exists: it defines canonical constants for JVM-global resources — `SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, `TIME_ZONE` — so that a test written by one team and a test written by another still agree on the key for "the JVM's system properties". For application-specific resources, define your own constants in a shared class rather than scattering string literals; a typo silently disables the protection, and nothing fails to alert you. ## Locking versus serialising Compare three ways of dealing with a shared resource: - **`@Execution(SAME_THREAD)`** — says the annotated node's children do not run concurrently *with each other*. It does **not** stop other classes running at the same time, so it does not protect global state at all. Using it as a fix for a cross-class race is a classic mistake. - **`@ResourceLock(key)`** — says "nothing else that declares this key runs at the same time". Scoped, cheap, and lets the rest of the suite proceed. - **Exclusive execution of a whole class** — says "nothing else in the suite runs at all". Correct but expensive, and reserved for state so pervasive you cannot name it. The middle option is the one that preserves throughput, which is the whole reason you turned parallelism on. ## Practical guidance - Annotate the smallest node that actually touches the resource. Method-level locks let a class's other tests keep running concurrently. - Use `READ` when the test only observes the resource; several readers then overlap. Reserve `READ_WRITE` for mutation. - Keep locked sections short. The lock is held for the whole node, so a slow test holding a hot key throttles everything that shares it. - Avoid declaring locks on a class *and* different locks on its methods; nested acquisition of different keys is the shape most likely to produce surprising stalls. - Treat every lock as documentation of a design smell. It records that two tests share mutable state. Sometimes that is unavoidable — the JVM default locale really is global — but often it is a hint that production code is reading configuration from a static instead of receiving it. ## Version note `@ResourceLock` and the `Resources` constants have been available since JUnit 5.3. Later versions added the ability to compute lock keys dynamically through a provider, which is useful when the resource depends on the test's parameters rather than being known at compile time.

  • Does @ResourceLock have any effect when parallel execution is disabled?
    No observable one. With everything running on a single thread there is never contention, so the locks are always immediately available. That is useful in practice: you can add the annotations while the suite is still sequential, and they become meaningful the day concurrency is switched on.
  • You annotate the class rather than the individual methods. What do you lose?
    Throughput. A class-level lock is held for the whole class, so the resource stays blocked for the duration of every test in it, including the ones that never touch it — and the annotated node's methods are put on one thread, so they no longer overlap each other either. Annotating only the methods that use the resource keeps the rest of the class parallel and shortens the critical section.

It is a hotel key for a named room: anyone who wants that room waits, everyone else keeps moving through the building. If two guests ask for the room by different names, they both walk in.

saying these in an interview costs you the question

  • Believing JUnit infers which resource a key refers to, or detects conflicts automatically
  • Using different spellings of the same key in two tests and expecting them to coordinate
  • Thinking @Execution(SAME_THREAD) protects global state from other classes
  • Assuming the lock covers only the test body and not @BeforeEach/@AfterEach
  • Locking every class 'to be safe', which reduces the suite to sequential execution

context