skip to content

What are @BeforeTransaction and @AfterTransaction for, and how do they order relative to @BeforeEach in a transactional test?

level: seniorimportance: should knowfreq 22%

answer

  1. Before/After the managed transaction, outside rollback
  2. @BeforeTransaction runs before @BeforeEach
  3. @BeforeEach runs INSIDE the tx (gets rolled back)
  4. Seed that must survive rollback -> @BeforeTransaction
  5. Only fire when test is @Transactional; void methods

basics

~20 s

@BeforeTransaction runs before the test's managed transaction starts; @AfterTransaction runs after it ends (commit or rollback). They let you set up or verify data outside the transaction — for example seeding rows that must survive rollback, or asserting the final committed state.

solid answer

~40 s

@BeforeTransaction and @AfterTransaction (from org.springframework.test.context.transaction) mark void methods that run outside the test-managed transaction: the former just before the transaction is started, the latter just after it is committed or rolled back. They only fire when the test method is transactional. The key ordering nuance: because Spring's SpringExtension starts the transaction inside its beforeEach callback — which runs before JUnit's @BeforeEach methods — @BeforeTransaction executes before @BeforeEach, and @BeforeEach therefore runs inside the transaction. Use @BeforeTransaction to prepare non-transactional fixtures or data that must persist across the rollback, and @AfterTransaction to assert on the committed outcome (paired with @Commit) or to clean up rows created outside the managed transaction. They're the hooks for anything that must sit outside the rollback boundary.

code

java · 29 lines
java
@SpringBootTest
@Transactional
class ReferenceDataTest {

    @Autowired JdbcTemplate jdbc;

    @BeforeTransaction // OUTSIDE the tx: commits in its own tx, survives the rollback
    void seedCountries() {
        jdbc.update("insert into country(code, name) values ('US','United States')");
    }

    @BeforeEach // INSIDE the tx: rolled back with the test
    void perTest() {
        jdbc.update("insert into visit(country_code) values ('US')");
    }

    @Test
    void usesSeededCountry() {
        Integer n = jdbc.queryForObject(
            "select count(*) from visit v join country c on v.country_code=c.code",
            Integer.class);
        assertThat(n).isEqualTo(1);
    } // <- transaction rolls back: the 'visit' row is gone, 'country' row remains

    @AfterTransaction // OUTSIDE the tx: clean up what @BeforeTransaction committed
    void removeCountries() {
        jdbc.update("delete from country where code = 'US'");
    }
}

go deeper

for a junior

Rarely expected; at most knows the hooks exist for setup/teardown around the transaction.

for a middle

Knows they run outside the transaction and require the test to be transactional.

for a senior

Explains the @BeforeTransaction-before-@BeforeEach ordering and the survives-rollback seeding pattern plus cleanup obligation.

for a principal

Uses them deliberately for cross-boundary fixtures and reasons about listener-driven lifecycle vs JUnit extension callbacks.

## What they are From `org.springframework.test.context.transaction`: - **`@BeforeTransaction`** — marks a `void` method executed **before** the test-managed transaction is started (for a test method configured to run transactionally). - **`@AfterTransaction`** — marks a `void` method executed **after** the test-managed transaction has ended (whether it committed or rolled back). Methods may have any visibility and must return `void`. They are invoked by the **`TransactionalTestExecutionListener`**. ## The crucial ordering Full lifecycle for one transactional test method under JUnit Jupiter + `SpringExtension`: ``` @BeforeTransaction <- outside the tx --- transaction STARTS --- @BeforeEach <- INSIDE the tx <test method body> <- INSIDE the tx @AfterEach <- INSIDE the tx --- transaction ENDS (rollback/commit) --- @AfterTransaction <- outside the tx ``` Why `@BeforeTransaction` beats `@BeforeEach`: `SpringExtension` implements `BeforeEachCallback`, and that callback (which drives `TestContextManager.beforeTestMethod`, starting the transaction and running `@BeforeTransaction`) fires **before** user `@BeforeEach` methods in JUnit's order. Consequently `@BeforeEach` runs *inside* the transaction and its writes are rolled back too — a frequent source of confusion ("why didn't my @BeforeEach seed survive?"). ## Why you need them Because everything inside the transaction is rolled back, sometimes you need to act **outside** the rollback boundary: - **`@BeforeTransaction`** - Seed reference data that must **survive the rollback** (it commits in its own transaction before the test's begins). - Prepare non-JPA resources (files, message queues, external state). - Verify pre-conditions in a clean, committed state. - **`@AfterTransaction`** - Assert the **final committed** state (meaningful only when the test used `@Commit`). - Clean up rows you created in `@BeforeTransaction` (they don't roll back — you own them). - Tear down external resources. ## Gotchas - They **do not run** if the test method isn't transactional. If you forget `@Transactional`, these hooks silently never fire. - Data written in `@BeforeTransaction` is **not** part of the rolled-back transaction, so it **persists** — you must clean it up in `@AfterTransaction` or it leaks. - Multiple `@BeforeTransaction`/`@AfterTransaction` methods can exist; use `@Order` (or JUnit method ordering isn't guaranteed) if sequence matters. - In `@AfterTransaction`, querying via the same `EntityManager`/repositories works but any changes there are again outside the managed transaction. - If a `@BeforeTransaction` method throws, the transaction never starts and the test is failed.

  • Does @BeforeEach run inside or outside the test-managed transaction?
    Inside. SpringExtension starts the transaction in its beforeEach callback, which precedes JUnit @BeforeEach methods, so their writes are also rolled back. @BeforeTransaction is the hook that runs before the transaction starts.
  • You seeded data in @BeforeTransaction — does it get rolled back with the test?
    No. It runs outside the managed transaction and commits separately, so it persists. You are responsible for removing it, typically in @AfterTransaction.

saying these in an interview costs you the question

  • Claiming @BeforeEach runs before the transaction starts
  • Assuming data seeded in @BeforeTransaction is rolled back with the test
  • Thinking these hooks fire even on non-transactional tests
  • Believing @AfterTransaction sees the same rolled-back transaction's uncommitted changes

context