Repository: junit-team/junit5 (language: java)

Issue:
Provide mechanism for managing resources across engines and executions
The Platform should provide an extensible mechanism (`ServiceLoader`?) so that user code can create "closeable resources" that are injected as part of the context received by `TestEngine` implementations.

This feature is analogous to the `ExtensionContext.Store` concept from Jupiter but takes it to the Platform level. Moreover, it allows to reuse the same resource across multiple executions in a `LauncherSession`. We should advocate that resources stored in such a key-value store be immutable until their destruction. If such resources implement `AutoCloseable` they will get closed immediately before the `LauncherSession` they belong to is closed.


Discussion / hints:
This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. Thank you for your contribution.

This issue has been automatically closed due to inactivity. If you have a good use case for this feature, please feel free to reopen the issue.

Hi @marcphilipp, if there is interest in this feature, I can take a stab at implementing this. Though I'm pretty new to the Junit codebase and would have to familiarize myself first.
> Hi @marcphilipp, if there is interest in this feature, I can take a stab at implementing this. Though I'm pretty new to the Junit codebase and would have to familiarize myself first.

I have interest, I'm interested in making an attempt at a change based on this for the cucumber jvm project.
@PinkFloyded @terencenmnpan Thanks for offering! Has either of you already put some thought into where/how you'd envision the Launcher API to change? I think starting on this will require a core team decision first but a concrete proposal would speed things up.
@marcphilipp @terencenmnpan @PinkFloyded 

any updates?
@YongGoose No, there are no updates. Are you interested in writing up a (short) proposal for discussion?
> [@YongGoose](https://github.com/YongGoose) No, there are no updates. Are you interested in writing up a (short) proposal for discussion?


Yes, I'm interested in writing up a proposal. However, it will take me a few days to conduct a thorough analysis and put together a detailed proposal.
Sure, no worries! 👍 
@marcphilipp 
I apologize for the delay in preparing the proposal.🥲
I’m still working through some thoughts and plan to start writing it after completing the following two tasks.
- https://github.com/junit-team/junit5/pull/4120
- https://github.com/junit-team/junit5/pull/4135

> I could work on the proposal at the same time, but I’ve decided to focus on the current tasks first to ensure the best quality for the project.
One idea I had in the meantime would be to build upon #3064. We could create both a session-level and request-level instance of `NamespacedHierarchicalStore`. The former could be exposed to registered `LauncherSessionListeners` via `LauncherSession.getStore()`. The latter would be created in the `Launcher` for every `LauncherDiscoveryRequest` and use the session-level store as its parent. It would then be exposed to test engines via `EngineDiscoveryRequest` and to `TestExecutionListeners` via `TestPlan`. JUnit Jupiter (and other engines) would then use the request-level store as parent when creating the store for `JupiterEngineExtensionContext`.

Fix patch:
diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc
index 0fc1d24435cd..1330707d6f4e 100644
--- a/documentation/src/docs/asciidoc/link-attributes.adoc
+++ b/documentation/src/docs/asciidoc/link-attributes.adoc
@@ -46,6 +46,7 @@ endif::[]
 :IterationSelector:                          {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector]
 :MethodSelector:                             {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector]
 :ModuleSelector:                             {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector]
+:NamespacedHierarchicalStore:                {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.html[NamespacedHierarchicalStore]
 :NestedClassSelector:                        {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector]
 :NestedMethodSelector:                       {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector]
 :OutputDirectoryProvider:                    {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider]
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc
index 464ac8fca81b..58da108e380b 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc
@@ -26,7 +26,15 @@ repository on GitHub.
 [[release-notes-5.13.0-M3-junit-platform-new-features-and-improvements]]
 ==== New Features and Improvements
 
-* ❓
+* Introduce resource management mechanism that allows preparing and sharing state across
+  executions or test engines via stores that are scoped to a `LauncherSession` or
+  `ExecutionRequest`. The Jupiter API uses these stores as ancestors to the `Store`
+  instances accessible via `ExtensionContext` and provides a new method to access them
+  directly. Please refer to the User Guide for examples of managing
+  <<../user-guide/index.adoc#launcher-api-launcher-session-listeners-tool-example-usage, session-scoped>>
+  and
+  <<../user-guide/index.adoc#launcher-api-managing-state-across-test-engines, request-scoped>>
+  resources.
 
 
 [[release-notes-5.13.0-M3-junit-jupiter]]
diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc
index e1fb5e37efca..9f8db34b21f1 100644
--- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc
@@ -1,3 +1,6 @@
+:testDir: ../../../../../src/test/java
+:testResourcesDir: ../../../../../src/test/resources
+
 [[launcher-api]]
 === JUnit Platform Launcher API
 
@@ -132,10 +135,22 @@ package example.session;
 
 include::{testDir}/example/session/GlobalSetupTeardownListener.java[tags=user_guide]
 ----
-<1> Start the HTTP server
-<2> Export its host address as a system property for consumption by tests
-<3> Export its port as a system property for consumption by tests
-<4> Stop the HTTP server
+<1> Get the store from the launcher session
+<2> Lazily create the HTTP server and put it into the store
+<3> Start the HTTP server
+
+It uses a wrapper class to ensure the server is stopped when the launcher session is
+closed:
+
+[source,java]
+.src/test/java/example/session/CloseableHttpServer.java
+----
+package example.session;
+
+include::{testDir}/example/session/CloseableHttpServer.java[tags=user_guide]
+----
+<1> The `close()` method is called when the launcher session is closed
+<2> Stop the HTTP server
 
 This sample uses the HTTP server implementation from the jdk.httpserver module that comes
 with the JDK but would work similarly with any other server or resource. In order for the
@@ -158,10 +173,11 @@ package example.session;
 
 include::{testDir}/example/session/HttpTests.java[tags=user_guide]
 ----
-<1> Read the host address of the server from the system property set by the listener
-<2> Read the port of the server from the system property set by the listener
-<3> Send a request to the server
-<4> Check the status code of the response
+<1> Retrieve the HTTP server instance from the store
+<2> Get the host string directly from the injected HTTP server instance
+<3> Get the port number directly from the injected HTTP server instance
+<4> Send a request to the server
+<5> Check the status code of the response
 
 [[launcher-api-launcher-interceptors-custom]]
 ==== Registering a LauncherInterceptor
@@ -285,3 +301,55 @@ execute any tests but will notify registered `{TestExecutionListener}` instances
 tests had been skipped and their containers had been successful. This can be useful to
 test changes in the configuration of a build or to verify a listener is called as expected
 without having to wait for all tests to be executed.
+
+[[launcher-api-managing-state-across-test-engines]]
+==== Managing State Across Test Engines
+
+When running tests on the JUnit Platform, multiple test engines may need to access shared
+resources. Rather than initializing these resources multiple times, JUnit Platform
+provides mechanisms to share state across test engines efficiently. Test engines can use
+the Platform's `{NamespacedHierarchicalStore}` API to lazily initialize and share
+resources, ensuring they are created only once regardless of execution order. Any resource
+that is put into the store and implements `AutoCloseable` will be closed automatically when
+the execution is finished.
+
+TIP: The Jupiter engine allows read and write access to such resources via its
+`{ExtensionContext_Store}` API.
+
+The following example demonstrates two custom test engines sharing a `ServerSocket`
+resource. `FirstCustomEngine` attempts to retrieve an existing `ServerSocket` from the
+global store or creates a new one if it doesn't exist:
+
+[source,java]
+----
+include::{testDir}/example/FirstCustomEngine.java[tags=user_guide]
+----
+
+`SecondCustomEngine` follows the same pattern, ensuring that regardless whether it runs
+before or after `FirstCustomEngine`, it will use the same socket instance:
+
+[source,java]
+----
+include::{testDir}/example/SecondCustomEngine.java[tags=user_guide]
+----
+
+TIP: In this case, the `ServerSocket` can be stored directly in the global store while
+ensuring since it gets closed because it implements `AutoCloseable`. If you need to use a
+type that does not do so, you can wrap it in a custom class that implements
+`AutoCloseable` and delegates to the original type. This is important to ensure that the
+resource is closed properly when the test run is finished.
+
+For illustration, the following test verifies that both engines are sharing the same
+`ServerSocket` instance and that it's closed after `Launcher.execute()` returns:
+
+[source,java,indent=0]
+----
+include::{testDir}/example/sharedresources/SharedResourceDemo.java[tags=user_guide]
+----
+
+By using the Platform's `{NamespacedHierarchicalStore}` API with shared namespaces in this
+way, test engines can coordinate resource creation and sharing without direct dependencies
+between them.
+
+Alternatively, it's possible to inject resources into test engines by
+<<launcher-api-launcher-session-listeners-custom, registering a `LauncherSessionListener`>>.
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
index 6d047292a98d..194215938d69 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
@@ -10,7 +10,9 @@
 
 package org.junit.jupiter.api.extension;
 
+import static java.util.Collections.unmodifiableList;
 import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+import static org.apiguardian.api.API.Status.INTERNAL;
 import static org.apiguardian.api.API.Status.STABLE;
 
 import java.lang.reflect.AnnotatedElement;
@@ -442,9 +444,28 @@ default void publishReportEntry(String value) {
 	 * @return the store in which to put and get objects for other invocations
 	 * working in the same namespace; never {@code null}
 	 * @see Namespace#GLOBAL
+	 * @see #getStore(StoreScope, Namespace)
 	 */
 	Store getStore(Namespace namespace);
 
+	/**
+	 * Returns the store for supplied scope and namespace.
+	 *
+	 * <p>If {@code scope} is
+	 * {@link StoreScope#EXTENSION_CONTEXT EXTENSION_CONTEXT}, the store behaves
+	 * exactly like the one returned by {@link #getStore(Namespace)}. If the
+	 * {@code scope} is {@link StoreScope#LAUNCHER_SESSION LAUNCHER_SESSION} or
+	 * {@link StoreScope#EXECUTION_REQUEST EXECUTION_REQUEST}, all stored values
+	 * that are instances of {@link AutoCloseable} are notified by invoking
+	 * their {@code close()} methods when the scope is closed.
+	 *
+	 * @since 5.13
+	 * @see StoreScope
+	 * @see #getStore(Namespace)
+	 */
+	@API(status = EXPERIMENTAL, since = "5.13")
+	Store getStore(StoreScope scope, Namespace namespace);
+
 	/**
 	 * Get the {@link ExecutionMode} associated with the current test or container.
 	 *
@@ -771,6 +792,57 @@ public Namespace append(Object... parts) {
 			Collections.addAll(newParts, parts);
 			return new Namespace(newParts);
 		}
+
+		@API(status = INTERNAL, since = "5.13")
+		public List<Object> getParts() {
+			return unmodifiableList(parts);
+		}
+	}
+
+	/**
+	 * {@code StoreScope} is an enumeration of the different scopes for
+	 * {@link Store} instances.
+	 *
+	 * @since 5.13
+	 * @see #getStore(StoreScope, Namespace)
+	 */
+	@API(status = EXPERIMENTAL, since = "5.13")
+	enum StoreScope {
+
+		/**
+		 * The store is scoped to the current {@code LauncherSession}.
+		 *
+		 * <p>Any data that is stored in a {@code Store} with this scope will be
+		 * available throughout the entire launcher session. Therefore, it may
+		 * be used to inject values from registered
+		 * {@code LauncherSessionListener} implementations, to share data across
+		 * multiple executions of the Jupiter engine within the same session, or
+		 * even to share data across multiple engines.
+		 *
+		 * @see org.junit.platform.launcher.LauncherSession#getStore()
+		 * @see org.junit.platform.launcher.LauncherSessionListener
+		 */
+		LAUNCHER_SESSION,
+
+		/**
+		 * The store is scoped to the current {@code ExecutionRequest} of the
+		 * JUnit Platform {@code Launcher}.
+		 *
+		 * <p>Any data that is stored in a {@code Store} with this scope will be
+		 * available for the duration of the current execution request.
+		 * Therefore, it may be used to share data across multiple engines.
+		 *
+		 * @see org.junit.platform.engine.ExecutionRequest#getStore()
+		 */
+		EXECUTION_REQUEST,
+
+		/**
+		 * The store is scoped to the current {@code ExtensionContext}.
+		 *
+		 * <p>Any data that is stored in a {@code Store} with this scope will be
+		 * bound to the current extension context lifecycle.
+		 */
+		EXTENSION_CONTEXT
 	}
 
 }
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
index 0de9bbb308ec..c3878b9c84b7 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
@@ -19,6 +19,7 @@
 import org.junit.jupiter.engine.config.DefaultJupiterConfiguration;
 import org.junit.jupiter.engine.config.JupiterConfiguration;
 import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
+import org.junit.jupiter.engine.descriptor.LauncherStoreFacade;
 import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver;
 import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
 import org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory;
@@ -82,8 +83,8 @@ protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest
 
 	@Override
 	protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) {
-		return new JupiterEngineExecutionContext(request.getEngineExecutionListener(),
-			getJupiterConfiguration(request));
+		return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), getJupiterConfiguration(request),
+			new LauncherStoreFacade(request.getStore()));
 	}
 
 	/**
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
index bf7cb5d9a059..84e716e01428 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
@@ -33,7 +33,6 @@
 import org.junit.jupiter.api.parallel.ExecutionMode;
 import org.junit.jupiter.engine.config.JupiterConfiguration;
 import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
-import org.junit.jupiter.engine.execution.NamespaceAwareStore;
 import org.junit.jupiter.engine.extension.ExtensionContextInternal;
 import org.junit.jupiter.engine.extension.ExtensionRegistry;
 import org.junit.platform.commons.JUnitException;
@@ -52,7 +51,8 @@
  */
 abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContextInternal, AutoCloseable {
 
-	private static final NamespacedHierarchicalStore.CloseAction<Namespace> CLOSE_RESOURCES = (__, ___, value) -> {
+	private static final NamespacedHierarchicalStore.CloseAction<org.junit.platform.engine.support.store.Namespace> CLOSE_RESOURCES = (
+			__, ___, value) -> {
 		if (value instanceof CloseableResource) {
 			((CloseableResource) value).close();
 		}
@@ -63,12 +63,14 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
 	private final T testDescriptor;
 	private final Set<String> tags;
 	private final JupiterConfiguration configuration;
-	private final NamespacedHierarchicalStore<Namespace> valuesStore;
+	private final NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> valuesStore;
 	private final ExecutableInvoker executableInvoker;
 	private final ExtensionRegistry extensionRegistry;
+	private final LauncherStoreFacade launcherStoreFacade;
 
 	AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
-			JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) {
+			JupiterConfiguration configuration, ExtensionRegistry extensionRegistry,
+			LauncherStoreFacade launcherStoreFacade) {
 
 		Preconditions.notNull(testDescriptor, "TestDescriptor must not be null");
 		Preconditions.notNull(configuration, "JupiterConfiguration must not be null");
@@ -78,8 +80,9 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
 		this.engineExecutionListener = engineExecutionListener;
 		this.testDescriptor = testDescriptor;
 		this.configuration = configuration;
-		this.valuesStore = createStore(parent);
+		this.valuesStore = createStore(parent, launcherStoreFacade);
 		this.extensionRegistry = extensionRegistry;
+		this.launcherStoreFacade = launcherStoreFacade;
 
 		// @formatter:off
 		this.tags = testDescriptor.getTags().stream()
@@ -88,9 +91,13 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
 		// @formatter:on
 	}
 
-	private static NamespacedHierarchicalStore<Namespace> createStore(ExtensionContext parent) {
-		NamespacedHierarchicalStore<Namespace> parentStore = null;
-		if (parent != null) {
+	private static NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> createStore(
+			ExtensionContext parent, LauncherStoreFacade launcherStoreFacade) {
+		NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> parentStore;
+		if (parent == null) {
+			parentStore = launcherStoreFacade.getRequestLevelStore();
+		}
+		else {
 			parentStore = ((AbstractExtensionContext<?>) parent).valuesStore;
 		}
 		return new NamespacedHierarchicalStore<>(parentStore, CLOSE_RESOURCES);
@@ -188,8 +195,21 @@ protected T getTestDescriptor() {
 
 	@Override
 	public Store getStore(Namespace namespace) {
-		Preconditions.notNull(namespace, "Namespace must not be null");
-		return new NamespaceAwareStore(this.valuesStore, namespace);
+		return launcherStoreFacade.getStoreAdapter(this.valuesStore, namespace);
+	}
+
+	@Override
+	public Store getStore(StoreScope scope, Namespace namespace) {
+		// TODO [#4246] Use switch expression
+		switch (scope) {
+			case LAUNCHER_SESSION:
+				return launcherStoreFacade.getSessionLevelStore(namespace);
+			case EXECUTION_REQUEST:
+				return launcherStoreFacade.getRequestLevelStore(namespace);
+			case EXTENSION_CONTEXT:
+				return getStore(namespace);
+		}
+		throw new JUnitException("Unknown StoreScope: " + scope);
 	}
 
 	@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
index 95b36fcda748..bad5573bf57d 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java
@@ -201,7 +201,7 @@ public final JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext
 		ThrowableCollector throwableCollector = createThrowableCollector();
 		ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(),
 			context.getExecutionListener(), this, this.classInfo.lifecycle, context.getConfiguration(), registry,
-			throwableCollector);
+			context.getLauncherStoreFacade(), throwableCollector);
 
 		// @formatter:off
 		return context.extend()
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
index 9350bfe4ea2d..cc052d0a60f5 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
@@ -37,9 +37,10 @@ final class ClassExtensionContext extends AbstractExtensionContext<ClassBasedTes
 
 	ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
 			ClassBasedTestDescriptor testDescriptor, Lifecycle lifecycle, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade,
+			ThrowableCollector throwableCollector) {
 
-		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 
 		this.lifecycle = lifecycle;
 		this.throwableCollector = throwableCollector;
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
index d04824dd223b..46fbb59d0559 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
@@ -31,8 +31,8 @@ final class ClassTemplateInvocationExtensionContext
 
 	ClassTemplateInvocationExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
 			ClassTemplateInvocationTestDescriptor testDescriptor, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry) {
-		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade) {
+		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 	}
 
 	@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java
index 2ca97029dc0b..c6774c9689d0 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java
@@ -119,7 +119,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
 			registry = childRegistry;
 		}
 		ExtensionContext extensionContext = new ClassTemplateInvocationExtensionContext(context.getExtensionContext(),
-			context.getExecutionListener(), this, context.getConfiguration(), registry);
+			context.getExecutionListener(), this, context.getConfiguration(), registry,
+			context.getLauncherStoreFacade());
 		ThrowableCollector throwableCollector = createThrowableCollector();
 		throwableCollector.execute(() -> this.invocationContext.prepareInvocation(extensionContext));
 		return context.extend() //
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
index fc87ef3f3a68..8f5a0ad0f911 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
@@ -29,8 +29,8 @@ class DynamicExtensionContext extends AbstractExtensionContext<DynamicNodeTestDe
 
 	DynamicExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
 			DynamicNodeTestDescriptor testDescriptor, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry) {
-		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade) {
+		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 	}
 
 	@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java
index 9ed1f81ca6b1..865e4044f52b 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java
@@ -46,7 +46,8 @@ public String getLegacyReportingName() {
 	@Override
 	public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
 		ExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(),
-			context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry());
+			context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry(),
+			context.getLauncherStoreFacade());
 		// @formatter:off
 		return context.extend()
 				.withExtensionContext(extensionContext)
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java
index 9491b89fc073..d9ecf220cdf7 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java
@@ -52,7 +52,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
 			context.getConfiguration());
 		EngineExecutionListener executionListener = context.getExecutionListener();
 		ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionListener, this,
-			context.getConfiguration(), extensionRegistry);
+			context.getConfiguration(), extensionRegistry, context.getLauncherStoreFacade());
 
 		// @formatter:off
 		return context.extend()
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
index 66ba8a94c7ac..8b647f4f95a9 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
@@ -31,9 +31,9 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext<Jupit
 
 	JupiterEngineExtensionContext(EngineExecutionListener engineExecutionListener,
 			JupiterEngineDescriptor testDescriptor, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry) {
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade) {
 
-		super(null, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+		super(null, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 	}
 
 	@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java
new file mode 100644
index 000000000000..212078d6f3e6
--- /dev/null
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.engine.descriptor;
+
+import static org.apiguardian.api.API.Status.INTERNAL;
+
+import org.apiguardian.api.API;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.engine.execution.NamespaceAwareStore;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+
+@API(status = INTERNAL, since = "5.13")
+public class LauncherStoreFacade {
+
+	private final NamespacedHierarchicalStore<Namespace> requestLevelStore;
+	private final NamespacedHierarchicalStore<Namespace> sessionLevelStore;
+
+	public LauncherStoreFacade(NamespacedHierarchicalStore<Namespace> requestLevelStore) {
+		this.requestLevelStore = requestLevelStore;
+		this.sessionLevelStore = requestLevelStore.getParent().orElseThrow(
+			() -> new JUnitException("Request-level store must have a parent"));
+	}
+
+	NamespacedHierarchicalStore<Namespace> getRequestLevelStore() {
+		return this.requestLevelStore;
+	}
+
+	ExtensionContext.Store getRequestLevelStore(ExtensionContext.Namespace namespace) {
+		return getStoreAdapter(this.requestLevelStore, namespace);
+	}
+
+	ExtensionContext.Store getSessionLevelStore(ExtensionContext.Namespace namespace) {
+		return getStoreAdapter(this.sessionLevelStore, namespace);
+	}
+
+	NamespaceAwareStore getStoreAdapter(NamespacedHierarchicalStore<Namespace> valuesStore,
+			ExtensionContext.Namespace namespace) {
+		Preconditions.notNull(namespace, "Namespace must not be null");
+		return new NamespaceAwareStore(valuesStore, convert(namespace));
+	}
+
+	private Namespace convert(ExtensionContext.Namespace namespace) {
+		return namespace.equals(ExtensionContext.Namespace.GLOBAL) //
+				? Namespace.GLOBAL //
+				: Namespace.create(namespace.getParts());
+	}
+}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
index 24cdd6914eb3..eaed0fe9418a 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
@@ -35,9 +35,10 @@ final class MethodExtensionContext extends AbstractExtensionContext<TestMethodTe
 
 	MethodExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
 			TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade,
+			ThrowableCollector throwableCollector) {
 
-		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 
 		this.throwableCollector = throwableCollector;
 	}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java
index 8d6f961e96fa..c6a416bd443a 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java
@@ -116,7 +116,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
 		MutableExtensionRegistry registry = populateNewExtensionRegistry(context);
 		ThrowableCollector throwableCollector = createThrowableCollector();
 		MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(),
-			context.getExecutionListener(), this, context.getConfiguration(), registry, throwableCollector);
+			context.getExecutionListener(), this, context.getConfiguration(), registry,
+			context.getLauncherStoreFacade(), throwableCollector);
 		// @formatter:off
 		JupiterEngineExecutionContext newContext = context.extend()
 				.withExtensionRegistry(registry)
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
index 5fa74917714b..9a30e567ba2b 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
@@ -32,9 +32,9 @@ final class TestTemplateExtensionContext extends AbstractExtensionContext<TestTe
 
 	TestTemplateExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
 			TestTemplateTestDescriptor testDescriptor, JupiterConfiguration configuration,
-			ExtensionRegistry extensionRegistry, TestInstances testInstances) {
+			ExtensionRegistry extensionRegistry, LauncherStoreFacade launcherStoreFacade, TestInstances testInstances) {
 
-		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
+		super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, launcherStoreFacade);
 		this.testInstances = testInstances;
 	}
 
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java
index 05ef02c0c991..90a0515e1e00 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java
@@ -94,7 +94,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
 		TestInstances testInstances = context.getExtensionContext().getTestInstances().orElse(null);
 
 		ExtensionContext extensionContext = new TestTemplateExtensionContext(context.getExtensionContext(),
-			context.getExecutionListener(), this, context.getConfiguration(), registry, testInstances);
+			context.getExecutionListener(), this, context.getConfiguration(), registry,
+			context.getLauncherStoreFacade(), testInstances);
 
 		// @formatter:off
 		return context.extend()
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java
index dc099bea9f44..fab10b76024e 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java
@@ -15,6 +15,7 @@
 import org.apiguardian.api.API;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.engine.config.JupiterConfiguration;
+import org.junit.jupiter.engine.descriptor.LauncherStoreFacade;
 import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
 import org.junit.platform.commons.JUnitException;
 import org.junit.platform.engine.EngineExecutionListener;
@@ -33,9 +34,9 @@ public class JupiterEngineExecutionContext implements EngineExecutionContext {
 	private boolean beforeAllCallbacksExecuted = false;
 	private boolean beforeAllMethodsExecuted = false;
 
-	public JupiterEngineExecutionContext(EngineExecutionListener executionListener,
-			JupiterConfiguration configuration) {
-		this(new State(executionListener, configuration));
+	public JupiterEngineExecutionContext(EngineExecutionListener executionListener, JupiterConfiguration configuration,
+			LauncherStoreFacade launcherStoreFacade) {
+		this(new State(executionListener, configuration, launcherStoreFacade));
 	}
 
 	private JupiterEngineExecutionContext(State state) {
@@ -62,6 +63,10 @@ public JupiterConfiguration getConfiguration() {
 		return this.state.configuration;
 	}
 
+	public LauncherStoreFacade getLauncherStoreFacade() {
+		return this.state.launcherStoreFacade;
+	}
+
 	public TestInstancesProvider getTestInstancesProvider() {
 		return this.state.testInstancesProvider;
 	}
@@ -119,14 +124,17 @@ private static final class State implements Cloneable {
 
 		final EngineExecutionListener executionListener;
 		final JupiterConfiguration configuration;
+		final LauncherStoreFacade launcherStoreFacade;
 		TestInstancesProvider testInstancesProvider;
 		MutableExtensionRegistry extensionRegistry;
 		ExtensionContext extensionContext;
 		ThrowableCollector throwableCollector;
 
-		State(EngineExecutionListener executionListener, JupiterConfiguration configuration) {
+		State(EngineExecutionListener executionListener, JupiterConfiguration configuration,
+				LauncherStoreFacade launcherStoreFacade) {
 			this.executionListener = executionListener;
 			this.configuration = configuration;
+			this.launcherStoreFacade = launcherStoreFacade;
 		}
 
 		@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java
index a39b4a189474..d63c7f13a940 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java
@@ -16,10 +16,10 @@
 import java.util.function.Supplier;
 
 import org.apiguardian.api.API;
-import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 import org.junit.jupiter.api.extension.ExtensionContext.Store;
 import org.junit.jupiter.api.extension.ExtensionContextException;
 import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStoreException;
 
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java
index 447790814427..3bdacace4401 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java
@@ -94,4 +94,5 @@ default OutputDirectoryProvider getOutputDirectoryProvider() {
 		throw new JUnitException(
 			"OutputDirectoryProvider not available; probably due to unaligned versions of the junit-platform-engine and junit-platform-launcher jars on the classpath/module path.");
 	}
+
 }
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
index 3d320a0d1c74..1267551da486 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
@@ -19,6 +19,8 @@
 import org.junit.platform.commons.PreconditionViolationException;
 import org.junit.platform.commons.util.Preconditions;
 import org.junit.platform.engine.reporting.OutputDirectoryProvider;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
 /**
  * Provides a single {@link TestEngine} access to the information necessary to
@@ -40,22 +42,25 @@ public class ExecutionRequest {
 	private final EngineExecutionListener engineExecutionListener;
 	private final ConfigurationParameters configurationParameters;
 	private final OutputDirectoryProvider outputDirectoryProvider;
+	private final NamespacedHierarchicalStore<Namespace> requestLevelStore;
 
 	@Deprecated
 	@API(status = DEPRECATED, since = "1.11")
 	public ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener,
 			ConfigurationParameters configurationParameters) {
-		this(rootTestDescriptor, engineExecutionListener, configurationParameters, null);
+		this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null);
 	}
 
 	private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener,
-			ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) {
+			ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider,
+			NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		this.rootTestDescriptor = Preconditions.notNull(rootTestDescriptor, "rootTestDescriptor must not be null");
 		this.engineExecutionListener = Preconditions.notNull(engineExecutionListener,
 			"engineExecutionListener must not be null");
 		this.configurationParameters = Preconditions.notNull(configurationParameters,
 			"configurationParameters must not be null");
 		this.outputDirectoryProvider = outputDirectoryProvider;
+		this.requestLevelStore = requestLevelStore;
 	}
 
 	/**
@@ -68,7 +73,7 @@ private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListe
 	 * engine may use to influence test execution
 	 * @return a new {@code ExecutionRequest}; never {@code null}
 	 * @since 1.9
-	 * @deprecated Use {@link #create(TestDescriptor, EngineExecutionListener, ConfigurationParameters, OutputDirectoryProvider)}
+	 * @deprecated without replacement
 	 */
 	@Deprecated
 	@API(status = DEPRECATED, since = "1.11")
@@ -88,16 +93,19 @@ public static ExecutionRequest create(TestDescriptor rootTestDescriptor,
 	 * engine may use to influence test execution; never {@code null}
 	 * @param outputDirectoryProvider {@link OutputDirectoryProvider} for
 	 * writing reports and other output files; never {@code null}
+	 * @param requestLevelStore {@link NamespacedHierarchicalStore} for storing
+	 * request-scoped data; never {@code null}
 	 * @return a new {@code ExecutionRequest}; never {@code null}
-	 * @since 1.12
+	 * @since 1.13
 	 */
-	@API(status = INTERNAL, since = "1.12")
+	@API(status = INTERNAL, since = "1.13")
 	public static ExecutionRequest create(TestDescriptor rootTestDescriptor,
 			EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters,
-			OutputDirectoryProvider outputDirectoryProvider) {
+			OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 
 		return new ExecutionRequest(rootTestDescriptor, engineExecutionListener, configurationParameters,
-			Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"));
+			Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"),
+			Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"));
 	}
 
 	/**
@@ -138,8 +146,25 @@ public ConfigurationParameters getConfigurationParameters() {
 	 */
 	@API(status = EXPERIMENTAL, since = "1.12")
 	public OutputDirectoryProvider getOutputDirectoryProvider() {
-		return Preconditions.notNull(outputDirectoryProvider,
+		return Preconditions.notNull(this.outputDirectoryProvider,
 			"No OutputDirectoryProvider was configured for this request");
 	}
 
+	/**
+	 * {@return the {@link NamespacedHierarchicalStore} for this request for
+	 * storing request-scoped data}
+	 *
+	 * <p>All stored values that implement {@link AutoCloseable} are notified by
+	 * invoking their {@code close()} methods when this request has been
+	 * executed.
+	 *
+	 * @since 1.13
+	 * @see NamespacedHierarchicalStore
+	 */
+	@API(status = EXPERIMENTAL, since = "1.13")
+	public NamespacedHierarchicalStore<Namespace> getStore() {
+		return Preconditions.notNull(this.requestLevelStore,
+			"No NamespacedHierarchicalStore was configured for this request");
+	}
+
 }
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java
new file mode 100644
index 000000000000..dcf06d571833
--- /dev/null
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.engine.support.store;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apiguardian.api.API;
+import org.junit.platform.commons.util.Preconditions;
+
+/**
+ * A {@code Namespace} is used to provide a <em>scope</em> for data saved by
+ * extensions within a {@link NamespacedHierarchicalStore}.
+ *
+ * <p>Storing data in custom namespaces allows extensions to avoid accidentally
+ * mixing data between extensions or across different invocations within the
+ * lifecycle of a single extension.
+ */
+@API(status = EXPERIMENTAL, since = "1.13")
+public class Namespace {
+
+	/**
+	 * The default, global namespace which allows access to stored data from
+	 * all extensions.
+	 */
+	public static final Namespace GLOBAL = Namespace.create(new Object());
+
+	/**
+	 * Create a namespace which restricts access to data to all extensions
+	 * which use the same sequence of {@code parts} for creating a namespace.
+	 *
+	 * <p>The order of the {@code parts} is significant.
+	 *
+	 * <p>Internally the {@code parts} are compared using {@link Object#equals(Object)}.
+	 */
+	public static Namespace create(Object... parts) {
+		Preconditions.notEmpty(parts, "parts array must not be null or empty");
+		Preconditions.containsNoNullElements(parts, "individual parts must not be null");
+		return new Namespace(Arrays.asList(parts));
+	}
+
+	/**
+	 * Create a namespace which restricts access to data to all extensions
+	 * which use the same sequence of {@code objects} for creating a namespace.
+	 *
+	 * <p>The order of the {@code objects} is significant.
+	 *
+	 * <p>Internally the {@code objects} are compared using {@link Object#equals(Object)}.
+	 */
+	public static Namespace create(List<Object> objects) {
+		Preconditions.notEmpty(objects, "objects list must not be null or empty");
+		Preconditions.containsNoNullElements(objects, "individual objects must not be null");
+		return new Namespace(objects);
+	}
+
+	private final List<Object> parts;
+
+	private Namespace(List<Object> parts) {
+		this.parts = new ArrayList<>(parts);
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (this == o) {
+			return true;
+		}
+		if (o == null || getClass() != o.getClass()) {
+			return false;
+		}
+		Namespace that = (Namespace) o;
+		return this.parts.equals(that.parts);
+	}
+
+	@Override
+	public int hashCode() {
+		return this.parts.hashCode();
+	}
+
+	/**
+	 * Create a new namespace by appending the supplied {@code parts} to the
+	 * existing sequence of parts in this namespace.
+	 *
+	 * @return new namespace; never {@code null}
+	 */
+	public Namespace append(Object... parts) {
+		Preconditions.notEmpty(parts, "parts array must not be null or empty");
+		Preconditions.containsNoNullElements(parts, "individual parts must not be null");
+		ArrayList<Object> newParts = new ArrayList<>(this.parts.size() + parts.length);
+		newParts.addAll(this.parts);
+		Collections.addAll(newParts, parts);
+		return new Namespace(newParts);
+	}
+}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java
index 9167dde42b13..bd27996973b1 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java
@@ -17,6 +17,7 @@
 
 import java.util.Comparator;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -85,6 +86,19 @@ public NamespacedHierarchicalStore<N> newChild() {
 		return new NamespacedHierarchicalStore<>(this, this.closeAction);
 	}
 
+	/**
+	 * Returns the parent store of this {@code NamespacedHierarchicalStore}.
+	 *
+	 * <p>If this store does not have a parent, an empty {@code Optional} is returned.
+	 *
+	 * @return an {@code Optional} containing the parent store, or an empty {@code Optional} if there is no parent
+	 * @since 5.13
+	 */
+	@API(status = EXPERIMENTAL, since = "5.13")
+	public Optional<NamespacedHierarchicalStore<N>> getParent() {
+		return Optional.ofNullable(this.parentStore);
+	}
+
 	/**
 	 * Determine if this store has been {@linkplain #close() closed}.
 	 *
@@ -447,6 +461,15 @@ public Failure(Throwable throwable) {
 	@FunctionalInterface
 	public interface CloseAction<N> {
 
+		@API(status = EXPERIMENTAL, since = "1.13")
+		static <N> CloseAction<N> closeAutoCloseables() {
+			return (__, ___, value) -> {
+				if (value instanceof AutoCloseable) {
+					((AutoCloseable) value).close();
+				}
+			};
+		}
+
 		/**
 		 * Close the supplied {@code value}.
 		 *
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java
index c78ed7761888..285621b59812 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java
@@ -10,9 +10,12 @@
 
 package org.junit.platform.launcher;
 
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
 import static org.apiguardian.api.API.Status.STABLE;
 
 import org.apiguardian.api.API;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.core.LauncherFactory;
 
 /**
@@ -47,4 +50,19 @@ public interface LauncherSession extends AutoCloseable {
 	@Override
 	void close();
 
+	/**
+	 * Get the {@link NamespacedHierarchicalStore} associated with this session.
+	 *
+	 * <p>All stored values that implement {@link AutoCloseable} are notified by
+	 * invoking their {@code close()} methods when this session is closed.
+	 *
+	 * <p>Any call to the store returned by this method after the session has
+	 * been closed will throw an exception.
+	 *
+	 * @since 1.13
+	 * @see NamespacedHierarchicalStore
+	 */
+	@API(status = EXPERIMENTAL, since = "1.13")
+	NamespacedHierarchicalStore<Namespace> getStore();
+
 }
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java
index 6bd73c4b641e..70106c48fb72 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java
@@ -103,4 +103,5 @@ public LauncherDiscoveryListener getDiscoveryListener() {
 	public OutputDirectoryProvider getOutputDirectoryProvider() {
 		return this.outputDirectoryProvider;
 	}
+
 }
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java
index 5ebc4f9a638e..2a9fa3bf1ed4 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java
@@ -11,6 +11,7 @@
 package org.junit.platform.launcher.core;
 
 import static java.util.Collections.unmodifiableCollection;
+import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables;
 import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY;
 import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION;
 
@@ -18,6 +19,8 @@
 
 import org.junit.platform.commons.util.Preconditions;
 import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryListener;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -41,6 +44,7 @@ class DefaultLauncher implements Launcher {
 	private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator(
 		listenerRegistry.testExecutionListeners);
 	private final EngineDiscoveryOrchestrator discoveryOrchestrator;
+	private final NamespacedHierarchicalStore<Namespace> sessionLevelStore;
 
 	/**
 	 * Construct a new {@code DefaultLauncher} with the supplied test engines.
@@ -50,7 +54,8 @@ class DefaultLauncher implements Launcher {
 	 * @param postDiscoveryFilters the additional post discovery filters for
 	 * discovery requests; never {@code null}
 	 */
-	DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> postDiscoveryFilters) {
+	DefaultLauncher(Iterable<TestEngine> testEngines, Collection<PostDiscoveryFilter> postDiscoveryFilters,
+			NamespacedHierarchicalStore<Namespace> sessionLevelStore) {
 		Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(),
 			() -> "Cannot create Launcher without at least one TestEngine; "
 					+ "consider adding an engine implementation JAR to the classpath");
@@ -59,6 +64,7 @@ class DefaultLauncher implements Launcher {
 			"PostDiscoveryFilter array must not contain null elements");
 		this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(testEngines,
 			unmodifiableCollection(postDiscoveryFilters), listenerRegistry.launcherDiscoveryListeners);
+		this.sessionLevelStore = sessionLevelStore;
 	}
 
 	@Override
@@ -100,7 +106,13 @@ private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryReque
 	}
 
 	private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) {
-		executionOrchestrator.execute(internalTestPlan, listeners);
+		try (NamespacedHierarchicalStore<Namespace> requestLevelStore = createRequestLevelStore()) {
+			executionOrchestrator.execute(internalTestPlan, requestLevelStore, listeners);
+		}
+	}
+
+	private NamespacedHierarchicalStore<Namespace> createRequestLevelStore() {
+		return new NamespacedHierarchicalStore<>(sessionLevelStore, closeAutoCloseables());
 	}
 
 }
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java
index 018eb41cf8a5..b128e09b1258 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java
@@ -10,10 +10,15 @@
 
 package org.junit.platform.launcher.core;
 
+import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables;
+
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryListener;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -40,21 +45,26 @@ public void close() {
 		}
 	};
 
+	private final NamespacedHierarchicalStore<Namespace> store = new NamespacedHierarchicalStore<>(null,
+		closeAutoCloseables());
 	private final LauncherInterceptor interceptor;
 	private final LauncherSessionListener listener;
 	private final DelegatingLauncher launcher;
 
-	DefaultLauncherSession(List<LauncherInterceptor> interceptors, Supplier<LauncherSessionListener> listenerSupplier,
-			Supplier<Launcher> launcherSupplier) {
+	DefaultLauncherSession(List<LauncherInterceptor> interceptors, //
+			Supplier<LauncherSessionListener> listenerSupplier, //
+			Function<NamespacedHierarchicalStore<Namespace>, Launcher> launcherFactory //
+	) {
 		interceptor = composite(interceptors);
 		Launcher launcher;
 		if (interceptor == NOOP_INTERCEPTOR) {
 			this.listener = listenerSupplier.get();
-			launcher = launcherSupplier.get();
+			launcher = launcherFactory.apply(this.store);
 		}
 		else {
 			this.listener = interceptor.intercept(listenerSupplier::get);
-			launcher = new InterceptingLauncher(interceptor.intercept(launcherSupplier::get), interceptor);
+			launcher = new InterceptingLauncher(interceptor.intercept(() -> launcherFactory.apply(this.store)),
+				interceptor);
 		}
 		this.launcher = new DelegatingLauncher(launcher);
 		listener.launcherSessionOpened(this);
@@ -74,10 +84,16 @@ public void close() {
 		if (launcher.delegate != ClosedLauncher.INSTANCE) {
 			launcher.delegate = ClosedLauncher.INSTANCE;
 			listener.launcherSessionClosed(this);
+			store.close();
 			interceptor.close();
 		}
 	}
 
+	@Override
+	public NamespacedHierarchicalStore<Namespace> getStore() {
+		return store;
+	}
+
 	private static class ClosedLauncher implements Launcher {
 
 		static final ClosedLauncher INSTANCE = new ClosedLauncher();
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java
index b2ed7779d471..00bcdae9ada8 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java
@@ -28,7 +28,6 @@
 import org.junit.platform.commons.logging.Logger;
 import org.junit.platform.commons.logging.LoggerFactory;
 import org.junit.platform.commons.util.UnrecoverableExceptions;
-import org.junit.platform.engine.EngineExecutionListener;
 import org.junit.platform.engine.Filter;
 import org.junit.platform.engine.FilterResult;
 import org.junit.platform.engine.TestDescriptor;
@@ -91,8 +90,8 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase
 	 * <p>Note: The test descriptors in the discovery result can safely be used
 	 * as non-root descriptors. Engine-test descriptor entries are pruned from
 	 * the returned result. As such execution by
-	 * {@link EngineExecutionOrchestrator#execute(LauncherDiscoveryResult, EngineExecutionListener)}
-	 * will not emit start or emit events for engines without tests.
+	 * {@link EngineExecutionOrchestrator} will not emit start or emit events
+	 * for engines without tests.
 	 */
 	public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, UniqueId parentId) {
 		LauncherDiscoveryResult result = discover(request, phase, parentId::appendEngine);
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java
index 9c5e1b3eaaf1..eccb29fd82df 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java
@@ -29,6 +29,8 @@
 import org.junit.platform.engine.TestEngine;
 import org.junit.platform.engine.TestExecutionResult;
 import org.junit.platform.engine.reporting.OutputDirectoryProvider;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.TestExecutionListener;
 import org.junit.platform.launcher.TestIdentifier;
 import org.junit.platform.launcher.TestPlan;
@@ -52,12 +54,14 @@ public EngineExecutionOrchestrator() {
 		this.listenerRegistry = listenerRegistry;
 	}
 
-	void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listeners) {
+	void execute(InternalTestPlan internalTestPlan, NamespacedHierarchicalStore<Namespace> requestLevelStore,
+			TestExecutionListener... listeners) {
 		ConfigurationParameters configurationParameters = internalTestPlan.getConfigurationParameters();
 		ListenerRegistry<TestExecutionListener> testExecutionListenerListeners = buildListenerRegistryForExecution(
 			listeners);
 		withInterceptedStreams(configurationParameters, testExecutionListenerListeners,
-			testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener));
+			testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener,
+				requestLevelStore));
 	}
 
 	/**
@@ -69,17 +73,18 @@ void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listene
 	 */
 	@API(status = INTERNAL, since = "1.9", consumers = { "org.junit.platform.suite.engine" })
 	public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener,
-			TestExecutionListener testExecutionListener) {
+			TestExecutionListener testExecutionListener, NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		Preconditions.notNull(discoveryResult, "discoveryResult must not be null");
 		Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null");
 		Preconditions.notNull(testExecutionListener, "testExecutionListener must not be null");
+		Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null");
 
 		InternalTestPlan internalTestPlan = InternalTestPlan.from(discoveryResult);
-		execute(internalTestPlan, engineExecutionListener, testExecutionListener);
+		execute(internalTestPlan, engineExecutionListener, testExecutionListener, requestLevelStore);
 	}
 
 	private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener parentEngineExecutionListener,
-			TestExecutionListener testExecutionListener) {
+			TestExecutionListener testExecutionListener, NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		internalTestPlan.markStarted();
 
 		// Do not directly pass the internal test plan to test execution listeners.
@@ -93,7 +98,8 @@ private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener
 		}
 		else {
 			execute(discoveryResult,
-				buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan));
+				buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan),
+				requestLevelStore);
 		}
 		testExecutionListener.testPlanExecutionFinished(testPlan);
 	}
@@ -153,7 +159,8 @@ private void withInterceptedStreams(ConfigurationParameters configurationParamet
 	 * EngineExecutionListener listener} of execution events.
 	 */
 	@API(status = INTERNAL, since = "1.7", consumers = { "org.junit.platform.testkit" })
-	public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener) {
+	public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener,
+			NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		Preconditions.notNull(discoveryResult, "discoveryResult must not be null");
 		Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null");
 
@@ -161,7 +168,7 @@ public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionList
 		EngineExecutionListener listener = selectExecutionListener(engineExecutionListener, configurationParameters);
 
 		for (TestEngine testEngine : discoveryResult.getTestEngines()) {
-			failOrExecuteEngine(discoveryResult, listener, testEngine);
+			failOrExecuteEngine(discoveryResult, listener, testEngine, requestLevelStore);
 		}
 	}
 
@@ -176,8 +183,7 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi
 	}
 
 	private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, EngineExecutionListener listener,
-			TestEngine testEngine) {
-
+			TestEngine testEngine, NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		EngineResultInfo engineDiscoveryResult = discoveryResult.getEngineResult(testEngine);
 		DiscoveryIssueNotifier discoveryIssueNotifier = engineDiscoveryResult.getDiscoveryIssueNotifier();
 		TestDescriptor engineDescriptor = engineDiscoveryResult.getRootDescriptor();
@@ -193,7 +199,7 @@ private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, Engine
 		}
 		else {
 			executeEngine(engineDescriptor, listener, discoveryResult.getConfigurationParameters(), testEngine,
-				discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier);
+				discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier, requestLevelStore);
 		}
 	}
 
@@ -207,13 +213,13 @@ private ListenerRegistry<TestExecutionListener> buildListenerRegistryForExecutio
 
 	private void executeEngine(TestDescriptor engineDescriptor, EngineExecutionListener listener,
 			ConfigurationParameters configurationParameters, TestEngine testEngine,
-			OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier) {
-
+			OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier,
+			NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener,
 			engineDescriptor);
 		try {
 			testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters,
-				outputDirectoryProvider));
+				outputDirectoryProvider, requestLevelStore));
 			discoveryIssueNotifier.logNonCriticalIssues(testEngine);
 			delayingListener.reportEngineOutcome();
 		}
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java
index c756f27351f0..b3d6ab2613a0 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java
@@ -26,6 +26,8 @@
 import org.junit.platform.commons.util.Preconditions;
 import org.junit.platform.engine.ConfigurationParameters;
 import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryListener;
 import org.junit.platform.launcher.LauncherInterceptor;
@@ -96,7 +98,8 @@ public static LauncherSession openSession(LauncherConfig config) throws Precondi
 		Preconditions.notNull(config, "LauncherConfig must not be null");
 		LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build();
 		return new DefaultLauncherSession(collectLauncherInterceptors(configurationParameters),
-			() -> createLauncherSessionListener(config), () -> createDefaultLauncher(config, configurationParameters));
+			() -> createLauncherSessionListener(config),
+			sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore));
 	}
 
 	/**
@@ -125,17 +128,17 @@ public static Launcher create() throws PreconditionViolationException {
 	public static Launcher create(LauncherConfig config) throws PreconditionViolationException {
 		Preconditions.notNull(config, "LauncherConfig must not be null");
 		LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build();
-		return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters),
+		return new SessionPerRequestLauncher(
+			sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore),
 			() -> createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters));
 	}
 
 	private static DefaultLauncher createDefaultLauncher(LauncherConfig config,
-			LauncherConfigurationParameters configurationParameters) {
+			LauncherConfigurationParameters configurationParameters,
+			NamespacedHierarchicalStore<Namespace> sessionLevelStore) {
 		Set<TestEngine> engines = collectTestEngines(config);
 		List<PostDiscoveryFilter> filters = collectPostDiscoveryFilters(config);
-
-		DefaultLauncher launcher = new DefaultLauncher(engines, filters);
-
+		DefaultLauncher launcher = new DefaultLauncher(engines, filters, sessionLevelStore);
 		registerLauncherDiscoveryListeners(config, launcher);
 		registerTestExecutionListeners(config, launcher, configurationParameters);
 
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java
index dffde014867a..cb12eafb7282 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java
@@ -11,8 +11,11 @@
 package org.junit.platform.launcher.core;
 
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryListener;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -28,14 +31,14 @@
 class SessionPerRequestLauncher implements Launcher {
 
 	private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry();
-	private final Supplier<Launcher> launcherSupplier;
+	private final Function<NamespacedHierarchicalStore<Namespace>, Launcher> launcherFactory;
 	private final Supplier<LauncherSessionListener> sessionListenerSupplier;
 	private final Supplier<List<LauncherInterceptor>> interceptorFactory;
 
-	SessionPerRequestLauncher(Supplier<Launcher> launcherSupplier,
+	SessionPerRequestLauncher(Function<NamespacedHierarchicalStore<Namespace>, Launcher> launcherFactory,
 			Supplier<LauncherSessionListener> sessionListenerSupplier,
 			Supplier<List<LauncherInterceptor>> interceptorFactory) {
-		this.launcherSupplier = launcherSupplier;
+		this.launcherFactory = launcherFactory;
 		this.sessionListenerSupplier = sessionListenerSupplier;
 		this.interceptorFactory = interceptorFactory;
 	}
@@ -73,7 +76,7 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
 
 	private LauncherSession createSession() {
 		LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier,
-			launcherSupplier);
+			this.launcherFactory);
 		Launcher launcher = session.getLauncher();
 		listenerRegistry.launcherDiscoveryListeners.getListeners().forEach(
 			launcher::registerLauncherDiscoveryListeners);
diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java
index c3a62006e859..6e3bf311ccfb 100644
--- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java
+++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java
@@ -19,6 +19,8 @@
 import org.junit.platform.engine.EngineExecutionListener;
 import org.junit.platform.engine.TestEngine;
 import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
 import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator;
 import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase;
@@ -58,9 +60,10 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq
 	}
 
 	TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult,
-			EngineExecutionListener parentEngineExecutionListener) {
+			EngineExecutionListener parentEngineExecutionListener,
+			NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		SummaryGeneratingListener listener = new SummaryGeneratingListener();
-		executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener);
+		executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore);
 		return listener.getSummary();
 	}
 
diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
index 0bcc205217a8..d9837a6a2319 100644
--- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
+++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
@@ -40,6 +40,8 @@
 import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter;
 import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector;
 import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.LauncherDiscoveryListener;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
 import org.junit.platform.launcher.core.LauncherDiscoveryResult;
@@ -144,13 +146,15 @@ private static String getSuiteDisplayName(Class<?> testClass) {
 		// @formatter:on
 	}
 
-	void execute(EngineExecutionListener parentEngineExecutionListener) {
+	void execute(EngineExecutionListener parentEngineExecutionListener,
+			NamespacedHierarchicalStore<Namespace> requestLevelStore) {
 		parentEngineExecutionListener.executionStarted(this);
 		ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector();
 
 		executeBeforeSuiteMethods(throwableCollector);
 
-		TestExecutionSummary summary = executeTests(parentEngineExecutionListener, throwableCollector);
+		TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore,
+			throwableCollector);
 
 		executeAfterSuiteMethods(throwableCollector);
 
@@ -171,7 +175,7 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
 	}
 
 	private TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener,
-			ThrowableCollector throwableCollector) {
+			NamespacedHierarchicalStore<Namespace> requestLevelStore, ThrowableCollector throwableCollector) {
 		if (throwableCollector.isNotEmpty()) {
 			return null;
 		}
@@ -181,7 +185,7 @@ private TestExecutionSummary executeTests(EngineExecutionListener parentEngineEx
 		// be pruned accordingly.
 		LauncherDiscoveryResult discoveryResult = this.launcherDiscoveryResult.withRetainedEngines(
 			getChildren()::contains);
-		return launcher.execute(discoveryResult, parentEngineExecutionListener);
+		return launcher.execute(discoveryResult, parentEngineExecutionListener, requestLevelStore);
 	}
 
 	private void executeAfterSuiteMethods(ThrowableCollector throwableCollector) {
diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java
index c0f754639c80..d75cf8c3dfbf 100644
--- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java
+++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java
@@ -22,6 +22,8 @@
 import org.junit.platform.engine.TestEngine;
 import org.junit.platform.engine.TestExecutionResult;
 import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
 /**
  * The JUnit Platform Suite {@link org.junit.platform.engine.TestEngine TestEngine}.
@@ -63,6 +65,7 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId
 	public void execute(ExecutionRequest request) {
 		SuiteEngineDescriptor suiteEngineDescriptor = (SuiteEngineDescriptor) request.getRootTestDescriptor();
 		EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener();
+		NamespacedHierarchicalStore<Namespace> requestLevelStore = request.getStore();
 
 		engineExecutionListener.executionStarted(suiteEngineDescriptor);
 
@@ -70,7 +73,7 @@ public void execute(ExecutionRequest request) {
 		suiteEngineDescriptor.getChildren()
 				.stream()
 				.map(SuiteTestDescriptor.class::cast)
-				.forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener));
+				.forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore));
 		// @formatter:on
 		engineExecutionListener.executionFinished(suiteEngineDescriptor, TestExecutionResult.successful());
 	}


Test patch:
diff --git a/documentation/src/test/java/example/FirstCustomEngine.java b/documentation/src/test/java/example/FirstCustomEngine.java
new file mode 100644
index 000000000000..efd9b14a7f0c
--- /dev/null
+++ b/documentation/src/test/java/example/FirstCustomEngine.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example;
+
+//tag::user_guide[]
+import static java.net.InetAddress.getLoopbackAddress;
+import static org.junit.platform.engine.TestExecutionResult.successful;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+
+import org.junit.platform.engine.EngineDiscoveryRequest;
+import org.junit.platform.engine.ExecutionRequest;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.descriptor.EngineDescriptor;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+
+/**
+ * First custom test engine implementation.
+ */
+public class FirstCustomEngine implements TestEngine {
+
+	public ServerSocket socket;
+
+	@Override
+	public String getId() {
+		return "first-custom-test-engine";
+	}
+
+	@Override
+	public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
+		return new EngineDescriptor(uniqueId, "First Custom Test Engine");
+	}
+
+	@Override
+	public void execute(ExecutionRequest request) {
+		request.getEngineExecutionListener()
+				// tag::custom_line_break[]
+				.executionStarted(request.getRootTestDescriptor());
+
+		NamespacedHierarchicalStore<Namespace> store = request.getStore();
+		socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
+			try {
+				return new ServerSocket(0, 50, getLoopbackAddress());
+			}
+			catch (IOException e) {
+				throw new UncheckedIOException("Failed to start ServerSocket", e);
+			}
+		}, ServerSocket.class);
+
+		request.getEngineExecutionListener()
+				// tag::custom_line_break[]
+				.executionFinished(request.getRootTestDescriptor(), successful());
+	}
+}
+//end::user_guide[]
diff --git a/documentation/src/test/java/example/SecondCustomEngine.java b/documentation/src/test/java/example/SecondCustomEngine.java
new file mode 100644
index 000000000000..3d11c13ac18d
--- /dev/null
+++ b/documentation/src/test/java/example/SecondCustomEngine.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example;
+
+import static java.net.InetAddress.getLoopbackAddress;
+import static org.junit.platform.engine.TestExecutionResult.successful;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+
+import org.junit.platform.engine.EngineDiscoveryRequest;
+import org.junit.platform.engine.ExecutionRequest;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.descriptor.EngineDescriptor;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+
+//tag::user_guide[]
+/**
+ * Second custom test engine implementation.
+ */
+public class SecondCustomEngine implements TestEngine {
+
+	public ServerSocket socket;
+
+	@Override
+	public String getId() {
+		return "second-custom-test-engine";
+	}
+
+	@Override
+	public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
+		return new EngineDescriptor(uniqueId, "Second Custom Test Engine");
+	}
+
+	@Override
+	public void execute(ExecutionRequest request) {
+		request.getEngineExecutionListener()
+				// tag::custom_line_break[]
+				.executionStarted(request.getRootTestDescriptor());
+
+		NamespacedHierarchicalStore<Namespace> store = request.getStore();
+		socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
+			try {
+				return new ServerSocket(0, 50, getLoopbackAddress());
+			}
+			catch (IOException e) {
+				throw new UncheckedIOException("Failed to start ServerSocket", e);
+			}
+		}, ServerSocket.class);
+
+		request.getEngineExecutionListener()
+				// tag::custom_line_break[]
+				.executionFinished(request.getRootTestDescriptor(), successful());
+	}
+}
+//end::user_guide[]
diff --git a/documentation/src/test/java/example/session/CloseableHttpServer.java b/documentation/src/test/java/example/session/CloseableHttpServer.java
new file mode 100644
index 000000000000..996fd85d8029
--- /dev/null
+++ b/documentation/src/test/java/example/session/CloseableHttpServer.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example.session;
+
+//tag::user_guide[]
+import java.util.concurrent.ExecutorService;
+
+import com.sun.net.httpserver.HttpServer;
+
+public class CloseableHttpServer implements AutoCloseable {
+
+	private final HttpServer server;
+	private final ExecutorService executorService;
+
+	CloseableHttpServer(HttpServer server, ExecutorService executorService) {
+		this.server = server;
+		this.executorService = executorService;
+	}
+
+	public HttpServer getServer() {
+		return server;
+	}
+
+	@Override
+	public void close() { // <1>
+		server.stop(0); // <2>
+		executorService.shutdownNow();
+	}
+}
+//end::user_guide[]
diff --git a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java
index 8db5232d5bcb..fdddad84ea4e 100644
--- a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java
+++ b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java
@@ -21,6 +21,8 @@
 
 import com.sun.net.httpserver.HttpServer;
 
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.LauncherSession;
 import org.junit.platform.launcher.LauncherSessionListener;
 import org.junit.platform.launcher.TestExecutionListener;
@@ -28,8 +30,6 @@
 
 public class GlobalSetupTeardownListener implements LauncherSessionListener {
 
-	private Fixture fixture;
-
 	@Override
 	public void launcherSessionOpened(LauncherSession session) {
 		// Avoid setup for test discovery by delaying it until tests are about to be executed
@@ -42,50 +42,28 @@ public void testPlanExecutionStarted(TestPlan testPlan) {
 					return;
 				}
 				//tag::user_guide[]
-				if (fixture == null) {
-					fixture = new Fixture();
-					fixture.setUp();
-				}
-			}
-		});
-	}
+				NamespacedHierarchicalStore<Namespace> store = session.getStore(); // <1>
+				store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2>
+					InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0);
+					HttpServer server;
+					try {
+						server = HttpServer.create(address, 0);
+					}
+					catch (IOException e) {
+						throw new UncheckedIOException("Failed to start HTTP server", e);
+					}
+					server.createContext("/test", exchange -> {
+						exchange.sendResponseHeaders(204, -1);
+						exchange.close();
+					});
+					ExecutorService executorService = Executors.newCachedThreadPool();
+					server.setExecutor(executorService);
+					server.start(); // <3>
 
-	@Override
-	public void launcherSessionClosed(LauncherSession session) {
-		if (fixture != null) {
-			fixture.tearDown();
-			fixture = null;
-		}
-	}
-
-	static class Fixture {
-
-		private HttpServer server;
-		private ExecutorService executorService;
-
-		void setUp() {
-			try {
-				server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0);
+					return new CloseableHttpServer(server, executorService);
+				});
 			}
-			catch (IOException e) {
-				throw new UncheckedIOException("Failed to start HTTP server", e);
-			}
-			server.createContext("/test", exchange -> {
-				exchange.sendResponseHeaders(204, -1);
-				exchange.close();
-			});
-			executorService = Executors.newCachedThreadPool();
-			server.setExecutor(executorService);
-			server.start(); // <1>
-			int port = server.getAddress().getPort();
-			System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); // <2>
-			System.setProperty("http.server.port", String.valueOf(port)); // <3>
-		}
-
-		void tearDown() {
-			server.stop(0); // <4>
-			executorService.shutdownNow();
-		}
+		});
 	}
 
 }
diff --git a/documentation/src/test/java/example/session/HttpTests.java b/documentation/src/test/java/example/session/HttpTests.java
index fdb560b66fa6..97a13439a3e4 100644
--- a/documentation/src/test/java/example/session/HttpTests.java
+++ b/documentation/src/test/java/example/session/HttpTests.java
@@ -13,25 +13,50 @@
 //tag::user_guide[]
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URL;
 
+import com.sun.net.httpserver.HttpServer;
+
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
 
+@ExtendWith(HttpServerParameterResolver.class)
 class HttpTests {
 
 	@Test
-	void respondsWith204() throws Exception {
-		String host = System.getProperty("http.server.host"); // <1>
-		String port = System.getProperty("http.server.port"); // <2>
+	void respondsWith204(HttpServer server) throws IOException {
+		String host = server.getAddress().getHostString(); // <2>
+		int port = server.getAddress().getPort(); // <3>
 		URL url = URI.create("http://" + host + ":" + port + "/test").toURL();
 
 		HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 		connection.setRequestMethod("GET");
-		int responseCode = connection.getResponseCode(); // <3>
+		int responseCode = connection.getResponseCode(); // <4>
+
+		assertEquals(204, responseCode); // <5>
+	}
+}
+
+class HttpServerParameterResolver implements ParameterResolver {
+	@Override
+	public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+		return HttpServer.class.equals(parameterContext.getParameter().getType());
+	}
 
-		assertEquals(204, responseCode); // <4>
+	@Override
+	public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+		return extensionContext
+				// tag::custom_line_break[]
+				.getStore(ExtensionContext.Namespace.GLOBAL)
+				// tag::custom_line_break[]
+				.get("httpServer", CloseableHttpServer.class) // <1>
+				.getServer();
 	}
 }
 //end::user_guide[]
diff --git a/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java
new file mode 100644
index 000000000000..52b00c624471
--- /dev/null
+++ b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example.sharedresources;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
+
+import example.FirstCustomEngine;
+import example.SecondCustomEngine;
+
+import org.junit.jupiter.api.Test;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+class SharedResourceDemo {
+
+	//tag::user_guide[]
+	@Test
+	void runBothCustomEnginesTest() {
+		FirstCustomEngine firstCustomEngine = new FirstCustomEngine();
+		SecondCustomEngine secondCustomEngine = new SecondCustomEngine();
+
+		Launcher launcher = LauncherFactory.create(LauncherConfig.builder()
+				// tag::custom_line_break[]
+				.addTestEngines(firstCustomEngine, secondCustomEngine)
+				// tag::custom_line_break[]
+				.enableTestEngineAutoRegistration(false)
+				// tag::custom_line_break[]
+				.build());
+
+		launcher.execute(request().build());
+
+		assertSame(firstCustomEngine.socket, secondCustomEngine.socket);
+		assertTrue(firstCustomEngine.socket.isClosed(), "socket should be closed");
+	}
+	//end::user_guide[]
+}
diff --git a/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java
new file mode 100644
index 000000000000..7beef2b5f0f1
--- /dev/null
+++ b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.launcher.core;
+
+import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables;
+
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+
+public class NamespacedHierarchicalStoreProviders {
+
+	public static NamespacedHierarchicalStore<Namespace> dummyNamespacedHierarchicalStore() {
+		return new NamespacedHierarchicalStore<>(dummyNamespacedHierarchicalStoreWithNoParent(), closeAutoCloseables());
+	}
+
+	public static NamespacedHierarchicalStore<Namespace> dummyNamespacedHierarchicalStoreWithNoParent() {
+		return new NamespacedHierarchicalStore<>(null, closeAutoCloseables());
+	}
+}
diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java
index df1505b1c4d8..9f7f3e18501a 100644
--- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java
+++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java
@@ -16,6 +16,7 @@
 import static org.apiguardian.api.API.Status.EXPERIMENTAL;
 import static org.apiguardian.api.API.Status.MAINTAINED;
 import static org.apiguardian.api.API.Status.STABLE;
+import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables;
 import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY;
 import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION;
 
@@ -23,6 +24,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.ServiceLoader;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 import org.apiguardian.api.API;
@@ -41,6 +43,8 @@
 import org.junit.platform.engine.TestEngine;
 import org.junit.platform.engine.UniqueId;
 import org.junit.platform.engine.reporting.OutputDirectoryProvider;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
 import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator;
 import org.junit.platform.launcher.core.EngineExecutionOrchestrator;
@@ -324,15 +328,30 @@ private static void executeDirectly(TestEngine testEngine, EngineDiscoveryReques
 			EngineExecutionListener listener) {
 		UniqueId engineUniqueId = UniqueId.forEngine(testEngine.getId());
 		TestDescriptor engineTestDescriptor = testEngine.discover(discoveryRequest, engineUniqueId);
-		ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener,
-			discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider());
-		testEngine.execute(request);
+		withRequestLevelStore(store -> {
+			ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener,
+				discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider(), store);
+			testEngine.execute(request);
+		});
 	}
 
 	private static void executeUsingLauncherOrchestration(TestEngine testEngine,
 			LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener) {
 		LauncherDiscoveryResult discoveryResult = discover(testEngine, discoveryRequest, EXECUTION);
-		new EngineExecutionOrchestrator().execute(discoveryResult, listener);
+		TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine);
+		Preconditions.notNull(engineTestDescriptor, "TestEngine did not yield a TestDescriptor");
+		withRequestLevelStore(store -> new EngineExecutionOrchestrator().execute(discoveryResult, listener, store));
+	}
+
+	private static void withRequestLevelStore(Consumer<NamespacedHierarchicalStore<Namespace>> action) {
+		try (NamespacedHierarchicalStore<Namespace> sessionLevelStore = newStore(null);
+				NamespacedHierarchicalStore<Namespace> requestLevelStore = newStore(sessionLevelStore)) {
+			action.accept(requestLevelStore);
+		}
+	}
+
+	private static NamespacedHierarchicalStore<Namespace> newStore(NamespacedHierarchicalStore<Namespace> parentStore) {
+		return new NamespacedHierarchicalStore<>(parentStore, closeAutoCloseables());
 	}
 
 	private static LauncherDiscoveryResult discover(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest,
diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java
index 2fd7a4b4080b..8811f97d404d 100644
--- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java
+++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java
@@ -16,6 +16,7 @@
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
+import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore;
 import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider;
 import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
 import static org.junit.platform.testkit.engine.EventConditions.container;
@@ -924,8 +925,9 @@ private static void execute(Class<?> testClass, EngineExecutionListener listener
 		TestEngine testEngine = new VintageTestEngine();
 		var discoveryRequest = request(testClass);
 		var engineTestDescriptor = testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId()));
-		testEngine.execute(ExecutionRequest.create(engineTestDescriptor, listener,
-			discoveryRequest.getConfigurationParameters(), dummyOutputDirectoryProvider()));
+		testEngine.execute(
+			ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters(),
+				dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore()));
 	}
 
 	private static LauncherDiscoveryRequest request(Class<?> testClass) {
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java
deleted file mode 100644
index dd242d20132a..000000000000
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2015-2025 the original author or authors.
- *
- * All rights reserved. This program and the accompanying materials are
- * made available under the terms of the Eclipse Public License v2.0 which
- * accompanies this distribution and is available at
- *
- * https://www.eclipse.org/legal/epl-v20.html
- */
-
-package org.junit.jupiter.engine;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * Basic assertions regarding {@link org.junit.platform.engine.TestEngine}
- * functionality in JUnit Jupiter.
- *
- * @since 5.0
- */
-class JupiterTestEngineBasicTests {
-
-	private final JupiterTestEngine jupiter = new JupiterTestEngine();
-
-	@Test
-	void id() {
-		assertEquals("junit-jupiter", jupiter.getId());
-	}
-
-	@Test
-	void groupId() {
-		assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow());
-	}
-
-	@Test
-	void artifactId() {
-		assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow());
-	}
-
-	@Test
-	void version() {
-		assertThat(jupiter.getVersion().orElseThrow()).isIn( //
-			System.getProperty("developmentVersion"), // with Test Distribution
-			"DEVELOPMENT" // without Test Distribution
-		);
-	}
-
-}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java
new file mode 100644
index 000000000000..79b2652c2028
--- /dev/null
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.engine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
+import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.engine.ConfigurationParameters;
+import org.junit.platform.engine.EngineExecutionListener;
+import org.junit.platform.engine.ExecutionRequest;
+import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders;
+
+/**
+ * @since  5.13
+ */
+public class JupiterTestEngineTests {
+
+	private final JupiterEngineDescriptor jupiterEngineDescriptor = mock();
+
+	private final ConfigurationParameters configurationParameters = mock();
+
+	private final EngineExecutionListener engineExecutionListener = mock();
+
+	private final ExecutionRequest executionRequest = mock();
+
+	private final JupiterTestEngine engine = new JupiterTestEngine();
+
+	private final JupiterTestEngine jupiter = new JupiterTestEngine();
+
+	@BeforeEach
+	void setUp() {
+		when(executionRequest.getEngineExecutionListener()).thenReturn(engineExecutionListener);
+		when(executionRequest.getConfigurationParameters()).thenReturn(configurationParameters);
+		when(executionRequest.getRootTestDescriptor()).thenReturn(jupiterEngineDescriptor);
+	}
+
+	@Test
+	void createExecutionContextWithValidRequest() {
+		when(executionRequest.getStore()).thenReturn(
+			NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore());
+
+		JupiterEngineExecutionContext context = engine.createExecutionContext(executionRequest);
+		assertThat(context).isNotNull();
+	}
+
+	@Test
+	void createExecutionContextWithNoParentsRequestLevelStore() {
+		when(executionRequest.getStore()).thenReturn(
+			NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStoreWithNoParent());
+
+		assertThatThrownBy(() -> engine //
+				.createExecutionContext(executionRequest)) //
+						.isInstanceOf(JUnitException.class) //
+						.hasMessageContaining("Request-level store must have a parent");
+	}
+
+	@Test
+	void id() {
+		assertEquals("junit-jupiter", jupiter.getId());
+	}
+
+	@Test
+	void groupId() {
+		assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow());
+	}
+
+	@Test
+	void artifactId() {
+		assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow());
+	}
+
+	@Test
+	void version() {
+		assertThat(jupiter.getVersion().orElseThrow()).isIn( //
+			System.getProperty("developmentVersion"), // with Test Distribution
+			"DEVELOPMENT" // without Test Distribution
+		);
+	}
+
+}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
index c011777ffa1f..2417b145c380 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
@@ -65,6 +65,7 @@
 import org.junit.platform.engine.reporting.FileEntry;
 import org.junit.platform.engine.reporting.ReportEntry;
 import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector;
+import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders;
 import org.mockito.ArgumentCaptor;
 
 /**
@@ -78,6 +79,8 @@ public class ExtensionContextTests {
 
 	private final JupiterConfiguration configuration = mock();
 	private final ExtensionRegistry extensionRegistry = mock();
+	private final LauncherStoreFacade launcherStoreFacade = new LauncherStoreFacade(
+		NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore());
 
 	@BeforeEach
 	void setUp() {
@@ -92,7 +95,7 @@ void fromJupiterEngineDescriptor() {
 		var engineTestDescriptor = new JupiterEngineDescriptor(UniqueId.root("engine", "junit-jupiter"), configuration);
 
 		try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration,
-			extensionRegistry)) {
+			extensionRegistry, launcherStoreFacade)) {
 			// @formatter:off
 			assertAll("engineContext",
 				() -> assertThat(engineContext.getElement()).isEmpty(),
@@ -123,7 +126,7 @@ void fromClassTestDescriptor() {
 		nestedClassDescriptor.addChild(methodTestDescriptor);
 
 		var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD,
-			configuration, extensionRegistry, null);
+			configuration, extensionRegistry, launcherStoreFacade, null);
 
 		// @formatter:off
 		assertAll("outerContext",
@@ -143,7 +146,7 @@ void fromClassTestDescriptor() {
 		// @formatter:on
 
 		var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor,
-			PER_METHOD, configuration, extensionRegistry, null);
+			PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null);
 		// @formatter:off
 		assertAll("nestedContext",
 			() -> assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext),
@@ -153,7 +156,7 @@ void fromClassTestDescriptor() {
 		// @formatter:on
 
 		var doublyNestedExtensionContext = new ClassExtensionContext(nestedExtensionContext, null,
-			doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, null);
+			doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null);
 		// @formatter:off
 		assertAll("doublyNestedContext",
 				() -> assertThat(doublyNestedExtensionContext.getParent()).containsSame(nestedExtensionContext),
@@ -163,7 +166,7 @@ void fromClassTestDescriptor() {
 		// @formatter:on
 
 		var methodExtensionContext = new MethodExtensionContext(nestedExtensionContext, null, methodTestDescriptor,
-			configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector());
+			configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector());
 		// @formatter:off
 		assertAll("methodContext",
 				() -> assertThat(methodExtensionContext.getParent()).containsSame(nestedExtensionContext),
@@ -177,7 +180,7 @@ void fromClassTestDescriptor() {
 	void ExtensionContext_With_ExtensionRegistry_getExtensions() {
 		var classTestDescriptor = nestedClassDescriptor();
 		try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration,
-			extensionRegistry, null)) {
+			extensionRegistry, launcherStoreFacade, null)) {
 
 			Extension ext = mock();
 			when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext));
@@ -195,18 +198,18 @@ void tagsCanBeRetrievedInExtensionContext() {
 		outerClassDescriptor.addChild(methodTestDescriptor);
 
 		var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD,
-			configuration, extensionRegistry, null);
+			configuration, extensionRegistry, launcherStoreFacade, null);
 
 		assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag");
 		assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext);
 
 		var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor,
-			PER_METHOD, configuration, extensionRegistry, null);
+			PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null);
 		assertThat(nestedExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "nested-tag");
 		assertThat(nestedExtensionContext.getRoot()).isSameAs(outerExtensionContext);
 
 		var methodExtensionContext = new MethodExtensionContext(outerExtensionContext, null, methodTestDescriptor,
-			configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector());
+			configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector());
 		methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase()));
 		assertThat(methodExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "method-tag");
 		assertThat(methodExtensionContext.getRoot()).isSameAs(outerExtensionContext);
@@ -224,11 +227,11 @@ void fromMethodTestDescriptor() {
 		var testMethod = methodTestDescriptor.getTestMethod();
 
 		var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration,
-			extensionRegistry);
+			extensionRegistry, launcherStoreFacade);
 		var classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, classTestDescriptor,
-			PER_METHOD, configuration, extensionRegistry, null);
+			PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null);
 		var methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, methodTestDescriptor,
-			configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector());
+			configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector());
 		methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance));
 
 		// @formatter:off
@@ -255,7 +258,7 @@ void reportEntriesArePublishedToExecutionListener() {
 		var classTestDescriptor = outerClassDescriptor(null);
 		var engineExecutionListener = spy(EngineExecutionListener.class);
 		ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener,
-			classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null);
+			classTestDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null);
 
 		var map1 = Collections.singletonMap("key", "value");
 		var map2 = Collections.singletonMap("other key", "other value");
@@ -381,7 +384,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir,
 		when(configuration.getOutputDirectoryProvider()) //
 				.thenReturn(hierarchicalOutputDirectoryProvider(tempDir));
 		return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, PER_METHOD, configuration,
-			extensionRegistry, null);
+			extensionRegistry, launcherStoreFacade, null);
 	}
 
 	@Test
@@ -390,9 +393,9 @@ void usingStore() {
 		var methodTestDescriptor = methodDescriptor();
 		var classTestDescriptor = outerClassDescriptor(methodTestDescriptor);
 		ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD,
-			configuration, extensionRegistry, null);
+			configuration, extensionRegistry, launcherStoreFacade, null);
 		var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration,
-			extensionRegistry, new OpenTest4JAwareThrowableCollector());
+			extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector());
 		childContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase()));
 
 		var childStore = childContext.getStore(Namespace.GLOBAL);
@@ -439,18 +442,20 @@ void configurationParameter(Function<JupiterConfiguration, ? extends ExtensionCo
 
 	static List<Named<Function<JupiterConfiguration, ? extends ExtensionContext>>> extensionContextFactories() {
 		ExtensionRegistry extensionRegistry = mock();
+		LauncherStoreFacade launcherStoreFacade = mock();
 		var testClass = ExtensionContextTests.class;
 		return List.of( //
 			named("engine", (JupiterConfiguration configuration) -> {
 				var engineUniqueId = UniqueId.parse("[engine:junit-jupiter]");
 				var engineDescriptor = new JupiterEngineDescriptor(engineUniqueId, configuration);
-				return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry);
+				return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry,
+					launcherStoreFacade);
 			}), //
 			named("class", (JupiterConfiguration configuration) -> {
 				var classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]");
 				var classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration);
 				return new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration,
-					extensionRegistry, null);
+					extensionRegistry, launcherStoreFacade, null);
 			}), //
 			named("method", (JupiterConfiguration configuration) -> {
 				var method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow();
@@ -458,7 +463,7 @@ void configurationParameter(Function<JupiterConfiguration, ? extends ExtensionCo
 				var methodTestDescriptor = new TestMethodTestDescriptor(methodUniqueId, testClass, method, List::of,
 					configuration);
 				return new MethodExtensionContext(null, null, methodTestDescriptor, configuration, extensionRegistry,
-					null);
+					launcherStoreFacade, null);
 			}) //
 		);
 	}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacadeTest.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacadeTest.java
new file mode 100644
index 000000000000..e140cb7329d5
--- /dev/null
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacadeTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.engine.descriptor;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.engine.execution.NamespaceAwareStore;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+
+/**
+ * Tests for {@link LauncherStoreFacade}.
+ *
+ * @since 5.13
+ */
+class LauncherStoreFacadeTest {
+
+	private NamespacedHierarchicalStore<Namespace> requestLevelStore;
+	private NamespacedHierarchicalStore<Namespace> sessionLevelStore;
+	private ExtensionContext.Namespace extensionNamespace;
+
+	@BeforeEach
+	void setUp() {
+		sessionLevelStore = new NamespacedHierarchicalStore<>(null);
+		requestLevelStore = new NamespacedHierarchicalStore<>(sessionLevelStore);
+		extensionNamespace = ExtensionContext.Namespace.create("foo", "bar");
+	}
+
+	@Test
+	void createsInstanceSuccessfullyWithValidStore() {
+		assertDoesNotThrow(() -> new LauncherStoreFacade(requestLevelStore));
+	}
+
+	@Test
+	void throwsExceptionWhenRequestLevelStoreHasNoParent() {
+		assertThrowsExactly(JUnitException.class, () -> new LauncherStoreFacade(sessionLevelStore), () -> {
+			throw new JUnitException("Request-level store must have a parent");
+		});
+	}
+
+	@Test
+	void returnsRequestLevelStore() {
+		LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore);
+		assertEquals(requestLevelStore, facade.getRequestLevelStore());
+	}
+
+	@Test
+	void returnsNamespaceAwareStoreWithRequestLevelStore() {
+		LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore);
+		ExtensionContext.Store store = facade.getRequestLevelStore(extensionNamespace);
+
+		assertNotNull(store);
+		assertInstanceOf(NamespaceAwareStore.class, store);
+	}
+
+	@Test
+	void returnsNamespaceAwareStore() {
+		LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore);
+		NamespaceAwareStore adapter = facade.getStoreAdapter(requestLevelStore, extensionNamespace);
+
+		assertNotNull(adapter);
+	}
+
+	@Test
+	void throwsExceptionWhenNamespaceIsNull() {
+		LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore);
+		assertThrows(PreconditionViolationException.class, () -> facade.getStoreAdapter(requestLevelStore, null));
+	}
+
+	@Test
+	void returnsNamespaceAwareStoreWithGlobalNamespace() {
+		requestLevelStore.put(Namespace.GLOBAL, "foo", "bar");
+
+		LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore);
+		ExtensionContext.Store store = facade.getRequestLevelStore(ExtensionContext.Namespace.GLOBAL);
+
+		assertEquals("bar", store.get("foo"));
+	}
+}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java
index 4ebb656d3f00..d072738824d9 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java
@@ -163,7 +163,7 @@ void before() throws Exception {
 			extensionContext = mock();
 			isClosed = false;
 
-			context = new JupiterEngineExecutionContext(null, null) //
+			context = new JupiterEngineExecutionContext(null, null, null) //
 					.extend() //
 					.withThrowableCollector(new OpenTest4JAwareThrowableCollector()) //
 					.withExtensionContext(extensionContext) //
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java
index 199b6903f46b..c39cc37aae7c 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java
@@ -16,8 +16,8 @@
 import java.util.stream.IntStream;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
 /**
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java
index 2c62a186b9fa..849d481212c6 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java
@@ -18,9 +18,9 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 import org.junit.jupiter.api.extension.ExtensionContext.Store;
 import org.junit.jupiter.api.extension.ExtensionContextException;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStoreException;
 
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java
index 80eeca958fbd..5e0b21658b32 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java
@@ -20,6 +20,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.engine.config.JupiterConfiguration;
+import org.junit.jupiter.engine.descriptor.LauncherStoreFacade;
 import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
 import org.junit.platform.engine.EngineExecutionListener;
 
@@ -34,8 +35,10 @@ class JupiterEngineExecutionContextTests {
 
 	private final EngineExecutionListener engineExecutionListener = mock();
 
+	private final LauncherStoreFacade launcherStoreFacade = mock();
+
 	private final JupiterEngineExecutionContext originalContext = new JupiterEngineExecutionContext(
-		engineExecutionListener, configuration);
+		engineExecutionListener, configuration, launcherStoreFacade);
 
 	@Test
 	void executionListenerIsHandedOnWhenContextIsExtended() {
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
index 37cfcdd57f92..5c66048d03f6 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
@@ -62,7 +62,6 @@
 import org.junit.jupiter.api.extension.AnnotatedElementContext;
 import org.junit.jupiter.api.extension.ExtensionConfigurationException;
 import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 import org.junit.jupiter.api.fixtures.TrackLogRecords;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -73,6 +72,7 @@
 import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.platform.commons.PreconditionViolationException;
 import org.junit.platform.commons.logging.LogRecordListener;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
 /**
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java
index 750611a5c67a..04e2735f4805 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java
@@ -25,6 +25,7 @@
 import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation;
 import org.junit.jupiter.engine.execution.NamespaceAwareStore;
 import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
 /**
@@ -71,7 +72,8 @@ void shouldThrowInvocationException() {
 
 	private static <T> SeparateThreadTimeoutInvocation<T> aSeparateThreadInvocation(Invocation<T> invocation) {
 		var namespace = ExtensionContext.Namespace.create(SeparateThreadTimeoutInvocationTests.class);
-		var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), namespace);
+		var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null),
+			Namespace.create(namespace.getParts()));
 		var parameters = new TimeoutInvocationParameters<>(invocation,
 			new TimeoutDuration(PREEMPTIVE_TIMEOUT_MILLIS, MILLISECONDS), () -> "method()",
 			PreInterruptCallbackInvocation.NOOP);
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java
index 234a2c19e63e..b630fd112054 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java
@@ -20,12 +20,12 @@
 import org.junit.jupiter.api.Timeout.ThreadMode;
 import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Store;
 import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation;
 import org.junit.jupiter.engine.execution.NamespaceAwareStore;
 import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.SingleThreadExecutorResource;
 import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.mockito.Mock;
 import org.mockito.Spy;
@@ -42,7 +42,7 @@ class TimeoutInvocationFactoryTests {
 
 	@Spy
 	private final Store store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null),
-		ExtensionContext.Namespace.create(TimeoutInvocationFactoryTests.class));
+		Namespace.create(TimeoutInvocationFactoryTests.class));
 
 	@Mock
 	private Invocation<String> invocation;
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
index 472429698597..24dba2894cd7 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
@@ -215,7 +215,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas
 
 		return new ExtensionContext() {
 
-			private final NamespacedHierarchicalStore<Namespace> store = new NamespacedHierarchicalStore<>(null);
+			private final NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> store = new NamespacedHierarchicalStore<>(
+				null);
 
 			@Override
 			public Optional<Method> getTestMethod() {
@@ -268,7 +269,7 @@ public Optional<Lifecycle> getTestInstanceLifecycle() {
 			}
 
 			@Override
-			public java.util.Optional<Object> getTestInstance() {
+			public Optional<Object> getTestInstance() {
 				return Optional.empty();
 			}
 
@@ -306,7 +307,8 @@ public void publishDirectory(String name, ThrowingConsumer<Path> action) {
 
 			@Override
 			public Store getStore(Namespace namespace) {
-				var store = new NamespaceAwareStore(this.store, namespace);
+				var store = new NamespaceAwareStore(this.store,
+					org.junit.platform.engine.support.store.Namespace.create(namespace.getParts()));
 				method //
 						.map(it -> new ParameterizedTestContext(testClass, it,
 							it.getAnnotation(ParameterizedTest.class))) //
@@ -314,6 +316,11 @@ public Store getStore(Namespace namespace) {
 				return store;
 			}
 
+			@Override
+			public Store getStore(StoreScope scope, Namespace namespace) {
+				return getStore(namespace);
+			}
+
 			@Override
 			public ExecutionMode getExecutionMode() {
 				return ExecutionMode.SAME_THREAD;
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java
index 09c83e273e60..85fb2c028746 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java
@@ -18,6 +18,7 @@
 import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
 import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL;
 import static org.junit.platform.engine.TestExecutionResult.successful;
+import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore;
 import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -80,7 +81,7 @@ void init() {
 	private HierarchicalTestExecutor<MyEngineExecutionContext> createExecutor(
 			HierarchicalTestExecutorService executorService) {
 		var request = ExecutionRequest.create(root, listener, mock(ConfigurationParameters.class),
-			dummyOutputDirectoryProvider());
+			dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore());
 		return new HierarchicalTestExecutor<>(request, rootContext, executorService,
 			OpenTest4JAwareThrowableCollector::new);
 	}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java
new file mode 100644
index 000000000000..512d2ad9fef8
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.engine.support.store;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode;
+
+import org.junit.jupiter.api.Test;
+
+public class NamespaceTests {
+
+	@Test
+	void namespacesEqualForSamePartsSequence() {
+		Namespace ns1 = Namespace.create("part1", "part2");
+		Namespace ns2 = Namespace.create("part1", "part2");
+		Namespace ns3 = Namespace.create("part2", "part1");
+
+		assertEqualsAndHashCode(ns1, ns2, ns3);
+	}
+
+	@Test
+	void orderOfNamespacePartsDoesMatter() {
+		Namespace ns1 = Namespace.create("part1", "part2");
+		Namespace ns2 = Namespace.create("part2", "part1");
+
+		assertNotEquals(ns1, ns2);
+	}
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java
index 77b14f19ca6a..81b1797c8b2b 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java
@@ -569,5 +569,4 @@ public String toString() {
 			}
 		};
 	}
-
 }
diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java
index 06f6c308242d..f1687ea26d97 100644
--- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java
@@ -28,6 +28,10 @@
 
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.StoreScope;
 import org.junit.jupiter.api.fixtures.TrackLogRecords;
 import org.junit.jupiter.engine.JupiterTestEngine;
 import org.junit.platform.commons.PreconditionViolationException;
@@ -37,11 +41,13 @@
 import org.junit.platform.engine.TestDescriptor;
 import org.junit.platform.engine.TestExecutionResult;
 import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.store.Namespace;
 import org.junit.platform.fakes.TestEngineSpy;
 import org.junit.platform.launcher.InterceptedTestEngine;
 import org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener;
 import org.junit.platform.launcher.LauncherConstants;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.LauncherSession;
 import org.junit.platform.launcher.LauncherSessionListener;
 import org.junit.platform.launcher.TagFilter;
 import org.junit.platform.launcher.TestExecutionListener;
@@ -333,6 +339,83 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult
 		}));
 	}
 
+	@Test
+	void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnOpened() {
+		var config = LauncherConfig.builder() //
+				.addLauncherSessionListeners(new LauncherSessionListenerOpenedExample()) //
+				.build();
+
+		try (LauncherSession session = LauncherFactory.openSession(config)) {
+			var launcher = session.getLauncher();
+			var request = request().selectors(selectClass(SessionTrackingTestCase.class)).build();
+
+			AtomicReference<Throwable> errorRef = new AtomicReference<>();
+			launcher.execute(request, new TestExecutionListener() {
+				@Override
+				public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+					testExecutionResult.getThrowable().ifPresent(errorRef::set);
+				}
+			});
+
+			assertThat(errorRef.get()).isNull();
+		}
+	}
+
+	@Test
+	void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnClose() {
+		var config = LauncherConfig.builder() //
+				.addLauncherSessionListeners(new LauncherSessionListenerClosedExample()) //
+				.build();
+
+		try (LauncherSession session = LauncherFactory.openSession(config)) {
+			var launcher = session.getLauncher();
+			var request = request().selectors(selectClass(SessionStoringTestCase.class)).build();
+
+			AtomicReference<Throwable> errorRef = new AtomicReference<>();
+			launcher.execute(request, new TestExecutionListener() {
+				@Override
+				public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+					testExecutionResult.getThrowable().ifPresent(errorRef::set);
+				}
+			});
+
+			assertThat(errorRef.get()).isNull();
+		}
+	}
+
+	@Test
+	void sessionResourceClosedOnSessionClose() {
+		CloseTrackingResource.closed = false;
+		var config = LauncherConfig.builder() //
+				.addLauncherSessionListeners(new AutoCloseCheckListener()) //
+				.build();
+
+		try (LauncherSession session = LauncherFactory.openSession(config)) {
+			var launcher = session.getLauncher();
+			var request = request().selectors(selectClass(SessionResourceAutoCloseTestCase.class)).build();
+
+			launcher.execute(request);
+			assertThat(CloseTrackingResource.closed).isFalse();
+		}
+
+		assertThat(CloseTrackingResource.closed).isTrue();
+	}
+
+	@Test
+	void requestResourceClosedOnExecutionClose() {
+		CloseTrackingResource.closed = false;
+		var config = LauncherConfig.builder().build();
+
+		try (LauncherSession session = LauncherFactory.openSession(config)) {
+			var launcher = session.getLauncher();
+			var request = request().selectors(selectClass(RequestResourceAutoCloseTestCase.class)).build();
+
+			launcher.execute(request);
+
+			assertThat(CloseTrackingResource.closed).isTrue();
+		}
+	}
+
 	@SuppressWarnings("SameParameterValue")
 	private static void withSystemProperty(String key, String value, Runnable runnable) {
 		var oldValue = System.getProperty(key);
@@ -390,6 +473,125 @@ static class JUnit5Example {
 		@Test
 		void testJ5() {
 		}
+	}
+
+	@SuppressWarnings("JUnitMalformedDeclaration")
+	@ExtendWith(SessionTrackingExtension.class)
+	static class SessionTrackingTestCase {
 
+		@Test
+		void dummyTest() {
+			// Just a placeholder to trigger the extension
+		}
+	}
+
+	@SuppressWarnings("JUnitMalformedDeclaration")
+	@ExtendWith(SessionStoringExtension.class)
+	static class SessionStoringTestCase {
+
+		@Test
+		void dummyTest() {
+			// Just a placeholder to trigger the extension
+		}
+	}
+
+	static class LauncherSessionListenerOpenedExample implements LauncherSessionListener {
+		@Override
+		public void launcherSessionOpened(LauncherSession session) {
+			session.getStore().put(Namespace.GLOBAL, "testKey", "testValue");
+		}
+	}
+
+	static class LauncherSessionListenerClosedExample implements LauncherSessionListener {
+		@Override
+		public void launcherSessionClosed(LauncherSession session) {
+			Object storedValue = session.getStore().get(Namespace.GLOBAL, "testKey");
+			assertThat(storedValue).isEqualTo("testValue");
+		}
+	}
+
+	static class SessionTrackingExtension implements BeforeAllCallback {
+		@Override
+		public void beforeAll(ExtensionContext context) {
+			var value = context.getStore(ExtensionContext.Namespace.GLOBAL).get("testKey");
+			if (!"testValue".equals(value)) {
+				throw new IllegalStateException("Expected 'testValue' but got: " + value);
+			}
+
+			value = context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).get("testKey");
+			if (!"testValue".equals(value)) {
+				throw new IllegalStateException("Expected 'testValue' but got: " + value);
+			}
+		}
+	}
+
+	static class SessionStoringExtension implements BeforeAllCallback {
+		@Override
+		public void beforeAll(ExtensionContext context) {
+			context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("testKey",
+				"testValue");
+		}
+	}
+
+	private static class CloseTrackingResource implements AutoCloseable {
+		private static boolean closed = false;
+
+		@Override
+		public void close() {
+			closed = true;
+		}
+
+		public boolean isClosed() {
+			return closed;
+		}
+	}
+
+	private static class SessionResourceStoreUsingExtension implements BeforeAllCallback {
+		@Override
+		public void beforeAll(ExtensionContext context) {
+			CloseTrackingResource sessionResource = new CloseTrackingResource();
+			context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("sessionResource",
+				sessionResource);
+		}
+	}
+
+	private static class RequestResourceStoreUsingExtension implements BeforeAllCallback {
+		@Override
+		public void beforeAll(ExtensionContext context) {
+			CloseTrackingResource requestResource = new CloseTrackingResource();
+			context.getStore(StoreScope.EXECUTION_REQUEST, ExtensionContext.Namespace.GLOBAL).put("requestResource",
+				requestResource);
+		}
+	}
+
+	@SuppressWarnings("JUnitMalformedDeclaration")
+	@ExtendWith(SessionResourceStoreUsingExtension.class)
+	static class SessionResourceAutoCloseTestCase {
+
+		@Test
+		void dummyTest() {
+			// Just a placeholder to trigger the extension
+		}
+	}
+
+	@SuppressWarnings("JUnitMalformedDeclaration")
+	@ExtendWith(RequestResourceStoreUsingExtension.class)
+	static class RequestResourceAutoCloseTestCase {
+
+		@Test
+		void dummyTest() {
+			// Just a placeholder to trigger the extension
+		}
+	}
+
+	private static class AutoCloseCheckListener implements LauncherSessionListener {
+		@Override
+		public void launcherSessionClosed(LauncherSession session) {
+			CloseTrackingResource sessionResource = session //
+					.getStore() //
+					.get(Namespace.GLOBAL, "sessionResource", CloseTrackingResource.class);
+
+			assertThat(sessionResource.isClosed()).isFalse();
+		}
 	}
 }
diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java
new file mode 100644
index 000000000000..043a88952cda
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.launcher.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.ExecutionRequest;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.fakes.TestEngineSpy;
+import org.junit.platform.fakes.TestEngineStub;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+
+/**
+ * @since 5.13
+ */
+class StoreSharingTests {
+
+	@Test
+	void twoDummyEnginesUseRequestLevelStore() {
+		TestEngineSpy engineWriter = new TestEngineSpy("Writer") {
+			@Override
+			public void execute(ExecutionRequest request) {
+				request.getStore().put(Namespace.GLOBAL, "sharedKey", "Hello from Writer");
+				super.execute(request);
+			}
+		};
+
+		TestEngineStub engineReader = new TestEngineStub("Reader") {
+			@Override
+			public void execute(ExecutionRequest request) {
+				Object value = request.getStore().get(Namespace.GLOBAL, "sharedKey");
+				assertEquals("Hello from Writer", value);
+				super.execute(request);
+			}
+		};
+
+		ExecutionRequest request = mock(ExecutionRequest.class);
+		when(request.getStore()).thenReturn(NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore());
+
+		Launcher launcher = LauncherFactory.create( //
+			LauncherConfig.builder() //
+					.addTestEngines(engineWriter, engineReader) //
+					.build());
+
+		LauncherDiscoveryRequest discoveryRequest = LauncherDiscoveryRequestBuilder //
+				.request() //
+				.build();
+
+		launcher.execute(discoveryRequest);
+	}
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java
index 589ddf72e73a..e776e897f20f 100644
--- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java
@@ -25,6 +25,9 @@
 import static org.junit.platform.testkit.engine.EventConditions.test;
 import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
 import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import java.nio.file.Path;
 
@@ -35,13 +38,20 @@
 import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.platform.engine.ConfigurationParameters;
 import org.junit.platform.engine.DiscoveryIssue;
 import org.junit.platform.engine.DiscoveryIssue.Severity;
+import org.junit.platform.engine.EngineExecutionListener;
+import org.junit.platform.engine.ExecutionRequest;
 import org.junit.platform.engine.FilterResult;
 import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.reporting.OutputDirectoryProvider;
 import org.junit.platform.engine.support.descriptor.ClassSource;
 import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 import org.junit.platform.launcher.PostDiscoveryFilter;
+import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders;
 import org.junit.platform.suite.api.SelectClasses;
 import org.junit.platform.suite.api.Suite;
 import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase;
@@ -624,6 +634,25 @@ void discoveryIssueOfNestedTestEnginesAreReported() throws Exception {
 	abstract private static class AbstractPrivateSuite {
 	}
 
+	@Test
+	void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
+		UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID);
+		SuiteEngineDescriptor engineDescriptor = new SuiteEngineDescriptor(engineId);
+
+		SuiteTestDescriptor mockDescriptor = mock(SuiteTestDescriptor.class);
+		engineDescriptor.addChild(mockDescriptor);
+
+		EngineExecutionListener listener = mock(EngineExecutionListener.class);
+		NamespacedHierarchicalStore<Namespace> requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore();
+
+		ExecutionRequest request = ExecutionRequest.create(engineDescriptor, listener,
+			mock(ConfigurationParameters.class), mock(OutputDirectoryProvider.class), requestLevelStore);
+
+		new SuiteTestEngine().execute(request);
+
+		verify(mockDescriptor).execute(same(listener), same(requestLevelStore));
+	}
+
 	@Suite
 	@SelectClasses(SingleTestTestCase.class)
 	private static class PrivateSuite {
diff --git a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java
index 55325c1962c4..60776a7b6c4e 100644
--- a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java
@@ -11,7 +11,14 @@
 package org.junit.platform.testkit.engine;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.mockito.ArgumentCaptor.forClass;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.Optional;
 import java.util.function.UnaryOperator;
@@ -23,7 +30,15 @@
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.platform.engine.TestEngine;
 import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.store.Namespace;
+import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
+import org.junit.platform.launcher.LauncherDiscoveryListener;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.core.EngineExecutionOrchestrator;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
 
 class EngineTestKitTests {
 
@@ -46,6 +61,29 @@ void ignoresImplicitConfigurationParametersByDefault() {
 		assertThat(value).isEmpty();
 	}
 
+	@Test
+	@SuppressWarnings("unchecked")
+	void verifyRequestLevelStoreIsUsedInExecution() {
+		TestEngine testEngine = mock(TestEngine.class);
+		when(testEngine.getId()).thenReturn("test-engine");
+
+		LauncherDiscoveryRequest request = mock(LauncherDiscoveryRequest.class);
+		when(request.getDiscoveryListener()).thenReturn(LauncherDiscoveryListener.NOOP);
+
+		try (MockedConstruction<EngineExecutionOrchestrator> mockedConstruction = mockConstruction(
+			EngineExecutionOrchestrator.class)) {
+			EngineTestKit.execute(testEngine, request);
+			assertThat(mockedConstruction.constructed()).isNotEmpty();
+
+			EngineExecutionOrchestrator mockOrchestrator = mockedConstruction.constructed().getFirst();
+			ArgumentCaptor<NamespacedHierarchicalStore<Namespace>> storeCaptor = forClass(
+				NamespacedHierarchicalStore.class);
+
+			verify(mockOrchestrator).execute(any(), any(), storeCaptor.capture());
+			assertNotNull(storeCaptor.getValue(), "Request level store should be passed to execute");
+		}
+	}
+
 	@ParameterizedTest
 	@CsvSource({ "true, from system property", "false," })
 	void usesImplicitConfigurationParametersWhenEnabled(boolean enabled, String expectedValue) {