Skip to content
Fresh

Singleton containers

Back

Guides

Testcontainers container lifecycle management using JUnit 5

Learn different approaches to manage container lifecycle with Testcontainers using JUnit 5 lifecycle callbacks, extension annotations, and the singleton containers pattern.

Java Testing with Docker

20 minutes

1

Create the project

2

Lifecycle callbacks

3

Extension annotations

4

Singleton containers

« Back to all guides

Singleton containers pattern

Copy as Markdown

Open MarkdownAsk Docs AIClaudeOpen in Claude

Table of contents


As the number of test classes grows, starting containers for each class adds up. The singleton containers pattern starts all required containers once in a common base class and reuses them across all integration tests.

Define the base class

Create an abstract base class that starts the containers in a static initializer:

java
package com.testcontainers.demo;

import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.kafka.ConfluentKafkaContainer;

public abstract class AbstractIntegrationTest {

   static PostgreSQLContainer postgres = new PostgreSQLContainer(
           "postgres:16-alpine");
   static ConfluentKafkaContainer kafka = new ConfluentKafkaContainer(
           "confluentinc/cp-kafka:7.8.0");

   static {
       postgres.start();
       kafka.start();
   }
}

The containers start once when the class loads and Testcontainers uses the Ryuk container to remove them after the JVM exits.

Tip

Instead of starting containers sequentially, start them in parallel using Startables.deepStart(postgres, kafka).join();

Extend the base class

Each test class inherits from the base class and reuses the same containers:

java
class ProductControllerTest extends AbstractIntegrationTest {

   ProductRepository productRepository;

   @BeforeEach
   void setUp() {
       productRepository = new ProductRepository(...);
       productRepository.deleteAll();
   }

   @Test
   void shouldGetAllProducts() {
       // test logic using the shared postgres container
   }
}

Avoid a common misconfiguration

A common mistake is combining singleton containers with the @Testcontainers and @Container annotations:

java
// DON'T DO THIS — containers will stop after each test class
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public abstract class AbstractIntegrationTest {

   @Container
   static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
           DockerImageName.parse("postgres:16-alpine"));

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

The @Testcontainers extension stops containers at the end of each testclass. Subsequent test classes reuse the cached Spring context, but the containers are already stopped — causing connection failures.

Instead, use a static initializer or @BeforeAll to start the containers, without the @Testcontainers and @Container annotations.

Summary

  • Use JUnit 5 lifecycle callbacks (@BeforeAll/@AfterAll) for explicit control over container startup and shutdown.
  • Use extension annotations (@Testcontainers/@Container) for less boilerplate in single test classes.
  • Use the singleton containers pattern (static initializer in a base class) to share containers across multiple test classes.
  • Don't mix singleton containers with @Testcontainers/@Container annotations.

Further reading