Share State Between Cucumber Steps Without Static Fields
Use Cucumber JVM dependency injection to share objects across step definitions within a scenario while keeping scenarios isolated.
10 Jan 2026, 13:43 UTC

The problem: static fields and flaky scenarios
When two step definition classes need to share data, the quickest workaround is often a static field. One step writes to it, a later step reads it. It works until it does not. Because Cucumber runs scenarios in the same JVM, static state survives past the end of a scenario. The next scenario can read stale values, and parallel execution can make the failures intermittent.
The useful takeaway is that Cucumber JVM can manage shared objects for you. Its dependency injection containers instantiate step definition classes and inject plain Java objects into their constructors. By default, the container is created fresh for every scenario, so shared state does not leak across scenario boundaries.
How Cucumber scopes injected state
Cucumber JVM supports dependency injection containers such as Picocontainer, Spring, and Guice. You add the matching integration module to your build, then declare dependencies in step definition constructors. The container resolves and supplies them.
By default, Cucumber creates a new container per scenario. Objects injected into step definitions are therefore scoped to that scenario. When the scenario finishes, the container is discarded. If you need the same object across multiple step definition classes within one scenario, inject the same POJO into both classes. If you need state to survive across scenarios, you must explicitly scope it as a singleton or store it somewhere static, but that reintroduces the leakage risk.
Worked example: Picocontainer with Maven
This example uses Cucumber JVM with Picocontainer, a lightweight container often used with Cucumber. Run these commands in your project root. No special permissions are required.
Add the integration module to your build file. Replace <cucumber.version> with the version your project already uses, and confirm the module version aligns with cucumber-core.
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version><cucumber.version></version>
<scope>test</scope>
</dependency>
Create a plain Java object to hold shared state. This is not a step definition; it is just a data holder.
public class BrowserSession {
private String currentUrl;
public String getCurrentUrl() {
return currentUrl;
}
public void setCurrentUrl(String currentUrl) {
this.currentUrl = currentUrl;
}
}
Inject it into two step definition classes. The container supplies the same instance to both within a single scenario.
public class NavigationSteps {
private final BrowserSession session;
public NavigationSteps(BrowserSession session) {
this.session = session;
}
@io.cucumber.java.en.Given("I am on {string}")
public void i_am_on(String url) {
session.setCurrentUrl(url);
}
}
public class AssertionSteps {
private final BrowserSession session;
public AssertionSteps(BrowserSession session) {
this.session = session;
}
@io.cucumber.java.en.Then("the URL should be {string}")
public void the_url_should_be(String expected) {
assert session.getCurrentUrl().equals(expected);
}
}
Expected check: run a scenario containing both steps. The second step should see the URL set by the first. Then run a second, unrelated scenario. It should receive a fresh BrowserSession with a null URL unless its own steps set one.
Risk: if you declare BrowserSession as a singleton in the container, every scenario shares the same instance and earlier scenarios can pollute later ones. Prefer the default scenario scope, or reset shared objects in a @Before hook.
Trade-offs and limitations
- Version coordinates are not portable. Cucumber-JVM major versions use different group IDs and package names. Import statements and dependency coordinates from a Cucumber 4 project will not work unchanged in Cucumber 7. Check the release notes for your major version before adding modules.
- DI does not remove fixture setup. Sharing an object does not mean scenarios can rely on each other's data. Each scenario should still set up its own preconditions.
- Spring users must handle rollback manually. With
cucumber-spring, database transaction rollback between scenarios is not automatic. Configure it separately if you need it. - Constructor injection requires the container. Without an integration module, Cucumber expects no-arg constructors, which often pushes teams toward static state workarounds.
Verify the result
- Run
mvn dependency:tree(Maven) orgradle dependencies(Gradle) and confirm the injection module version matchescucumber-core. - Add a scenario that sets shared state in one step and asserts it in another step defined in a different class.
- Add a second scenario that does not set that state and assert the injected object starts empty.
- If the second scenario sees data from the first, check whether the object was accidentally scoped as a singleton.
Closing
Dependency injection in Cucumber JVM is not about making tests run faster. It is about removing static fields and hidden coupling between step definitions. Use the default scenario-scoped container to share data within a scenario, keep singletons out of your step state, and verify that each scenario starts with a clean object graph.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.