> parseProviders(
for (String key : section.keys()) {
Class> cls;
try {
- cls = RenameHandler.INSTANCE.lookupType(key);
+ cls = Class.forName(key, true, ExtendedEnum.class.getClassLoader());
} catch (Exception ex) {
throw new IllegalArgumentException("Unable to find enum provider class: " + key, ex);
}
diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java
new file mode 100644
index 0000000000..886339554e
--- /dev/null
+++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies
+ *
+ * Please see distribution for license.
+ */
+package com.opengamma.strata.collect.named;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinWorkerThread;
+
+/**
+ * Test that reproduces ExtendedEnum failures in isolated classloader hierarchies.
+ *
+ * This program recreates the classloader structure found in application servers,
+ * OSGi containers, and multi-module applications where libraries are separated
+ * into parent (common/shared) and child (application-specific) classloaders:
+ *
+ * Parent ClassLoader (Common/Shared Libraries)
+ *
+ * - Joda-Convert (includes RenameHandler)
+ * - Joda-Beans
+ * - Guava
+ *
+ *
+ * Child ClassLoader (Application Libraries)
+ *
+ * - Strata modules (ExtendedEnum, StandardDayCounts, etc.)
+ * - Delegates to parent for common libraries
+ *
+ *
+ * ForkJoinPool Worker Threads - Thread Context ClassLoader (TCCL) = null
+ *
+ * When ExtendedEnum.parseProviders() (loaded in child) calls RenameHandler.lookupType()
+ * (loaded in parent) to find StandardDayCounts:
+ *
+ * - RenameHandler checks TCCL → null → unavailable
+ * - RenameHandler uses its own classloader → parent
+ * - Parent classloader cannot see child classes → ClassNotFoundException
+ *
+ *
+ * This reproduces the exact failure reported in production environments.
+ *
+ * Usage:
+ *
+ * java ExtendedEnumClassLoaderIsolationTest <strata-module-jar> [<strata-module-jar> ...]
+ *
+ *
+ * Arguments should be Strata module JARs to load in the child classloader (the code under test).
+ * Parent classloader dependencies (Joda-Convert, Guava, etc.) are automatically discovered from
+ * the local Maven repository to simulate production application server environments.
+ *
+ * This test is executed automatically during Maven's verify phase via exec-maven-plugin.
+ */
+public class ExtendedEnumClassLoaderIsolationTest {
+
+ public static void main(String[] args) throws Exception {
+ if (args.length < 1) {
+ System.err.println("Usage: ExtendedEnumClassLoaderIsolationTest [ ...]");
+ System.err.println();
+ System.err.println("Arguments: Strata module JARs to load in CHILD classloader (the code under test)");
+ System.err.println("Note: Parent classloader dependencies (Joda-Convert, Guava, etc.) are automatically");
+ System.err.println(" discovered from the local Maven repository to simulate production environments.");
+ System.exit(1);
+ }
+
+ System.out.println("ExtendedEnum ClassLoader Isolation Test");
+ System.out.println("Testing ExtendedEnum initialization in isolated classloader hierarchy");
+
+ String m2Repo = System.getProperty("user.home") + "/.m2/repository";
+
+ // PARENT CLASSLOADER: Common/shared libraries (like in app server's lib/)
+ // This simulates the "common" classloader in Tomcat, or shared bundle in OSGi
+ List parentJars = new ArrayList<>();
+ addJarsFromDir(parentJars, new File(m2Repo + "/org/joda/joda-convert"));
+ addJarsFromDir(parentJars, new File(m2Repo + "/org/joda/joda-beans"));
+ addJarsFromDir(parentJars, new File(m2Repo + "/com/google/guava"));
+ addJarsFromDir(parentJars, new File(m2Repo + "/com/google/code/findbugs"));
+ addJarsFromDir(parentJars, new File(m2Repo + "/com/google/errorprone"));
+
+ System.out.println(" Parent classloader: " + parentJars.size() + " JARs (Joda-Convert, Joda-Beans, Guava)");
+
+ URLClassLoader parentCL = new URLClassLoader(
+ parentJars.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
+
+ // CHILD CLASSLOADER: Application classes (like in app server's webapps/)
+ // This simulates the "webapp" classloader in Tomcat, or application bundle in OSGi
+ List childJars = new ArrayList<>();
+ for (String jarPath : args) {
+ addIfExists(childJars, jarPath);
+ }
+
+ System.out.println(" Child classloader: " + childJars.size() + " JARs (Strata modules)");
+
+ URLClassLoader childCL = new URLClassLoader(
+ childJars.toArray(new URL[0]), parentCL); // Parent can see Joda-Convert, but child has Strata
+
+ // Verify the hierarchy is correctly isolated
+ parentCL.loadClass("org.joda.convert.RenameHandler");
+ childCL.loadClass("com.opengamma.strata.collect.named.ExtendedEnum");
+ try {
+ parentCL.loadClass("com.opengamma.strata.basics.date.StandardDayCounts");
+ throw new AssertionError("Parent classloader should not see StandardDayCounts");
+ } catch (ClassNotFoundException ignored) {
+ // Expected - parent cannot see child classes
+ }
+ childCL.loadClass("com.opengamma.strata.basics.date.StandardDayCounts");
+ System.out.println(" Classloader isolation verified");
+
+ ForkJoinPool pool = new ForkJoinPool(
+ 1,
+ poolArg -> {
+ ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(poolArg);
+ thread.setContextClassLoader(null); // NULL TCCL
+ return thread;
+ },
+ null,
+ false);
+
+ try {
+ Callable task = () -> {
+ try {
+ Class> dayCountClass = childCL.loadClass("com.opengamma.strata.basics.date.DayCount");
+ Method extendedEnumMethod = dayCountClass.getMethod("extendedEnum");
+ extendedEnumMethod.invoke(null);
+ return null;
+ } catch (Exception e) {
+ // Extract root cause
+ Throwable cause = e;
+ while (cause.getCause() != null) {
+ cause = cause.getCause();
+ }
+
+ if (cause instanceof ClassNotFoundException) {
+ throw new AssertionError(
+ "ClassNotFoundException in ForkJoinPool with isolated classloaders. " +
+ "This indicates issue #2748 has regressed. " +
+ "ExtendedEnum failed to load: " + cause.getMessage(), cause);
+ }
+
+ throw e;
+ }
+ };
+
+ pool.submit(task).get();
+ System.out.println(" ✓ ExtendedEnum initialization successful in ForkJoinPool with null TCCL");
+ System.out.println(" ✓ Test passed");
+
+ } catch (Exception e) {
+ System.err.println("TEST FAILED:");
+ e.printStackTrace(System.err);
+ System.exit(1);
+ } finally {
+ pool.shutdown();
+ }
+ }
+
+ private static void addIfExists(List urls, String path) {
+ File file = new File(path);
+ if (file.exists()) {
+ try {
+ urls.add(file.toURI().toURL());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static void addJarsFromDir(List urls, File dir) {
+ if (!dir.exists()) {
+ return;
+ }
+ addJarsRecursive(urls, dir);
+ }
+
+ private static void addJarsRecursive(List urls, File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return;
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ addJarsRecursive(urls, file);
+ } else if (file.getName().endsWith(".jar")) {
+ try {
+ urls.add(file.toURI().toURL());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java
new file mode 100644
index 0000000000..7b2734463c
--- /dev/null
+++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies
+ *
+ * Please see distribution for license.
+ */
+package com.opengamma.strata.collect.named;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test {@link ExtendedEnum} thread safety and initialization patterns in concurrent contexts.
+ *
+ * These tests verify that ExtendedEnum can be safely initialized from various threading
+ * constructs including ForkJoinPool, parallel streams, and CompletableFuture. This is
+ * particularly important in production environments where complex classloader hierarchies
+ * may cause initialization failures when the thread context classloader is unavailable.
+ *
+ * Important: These JUnit tests run within Maven's Surefire plugin, which uses a
+ * simplified classloader hierarchy where all dependencies (including Joda-Convert and
+ * Strata modules) are loaded by the same classloader. This means these tests cannot
+ * reproduce the production ClassNotFoundException reported in issue #2748.
+ *
+ * The actual bug reproduction requires a separate JVM process with an isolated parent/child
+ * classloader hierarchy. See {@link ExtendedEnumClassLoaderIsolationTest} which runs during
+ * the Maven verify phase to reproduce the issue. That test creates the exact classloader
+ * structure found in application servers (Tomcat, WebLogic) and OSGi containers where the
+ * bug occurs.
+ *
+ * These tests remain valuable for verifying thread-safety patterns and ensuring ExtendedEnum
+ * works correctly in various concurrent execution contexts.
+ *
+ * These tests use {@link ThreadSafeSampleNamed} to ensure clean initialization scenarios
+ * independent of other tests in the suite.
+ *
+ * @see Issue #2748
+ */
+public class ExtendedEnumThreadSafetyTest {
+
+ /**
+ * Test that ExtendedEnum can be initialized from within a ForkJoinPool.
+ *
+ * This test verifies the pattern that fails in production: when DayCounts (or any ExtendedEnum)
+ * is first accessed from a ForkJoinPool worker thread. However, this test cannot reproduce
+ * issue #2748 because Maven's classloader hierarchy doesn't match production environments.
+ * See the class-level javadoc for details on why {@link ExtendedEnumClassLoaderIsolationTest}
+ * is needed.
+ */
+ @Test
+ public void test_initialization_in_forkjoin_pool() throws Exception {
+ ForkJoinPool pool = new ForkJoinPool(4);
+ try {
+ // Submit a task that accesses ExtendedEnum for the first time
+ ForkJoinTask> task = pool.submit(
+ () -> ExtendedEnum.of(ThreadSafeSampleNamed.class));
+
+ ExtendedEnum result = task.get(5, TimeUnit.SECONDS);
+
+ // Verify the ExtendedEnum was properly initialized
+ assertThat(result).isNotNull();
+ assertThat(result.lookupAll()).isNotEmpty();
+ assertThat(result.lookup("ThreadStandard")).isNotNull();
+ assertThat(result.lookup("ThreadStandard").getName()).isEqualTo("ThreadStandard");
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Test that ExtendedEnum can be used in parallel streams.
+ *
+ * Parallel streams use ForkJoinPool.commonPool() which has the same classloader issue.
+ */
+ @Test
+ public void test_initialization_in_parallel_stream() {
+ List names = Arrays.asList("ThreadStandard", "ThreadMore", "ThreadStandard");
+
+ // Use parallel stream which internally uses ForkJoinPool.commonPool()
+ List results = names.parallelStream()
+ .map(name -> ThreadSafeSampleNamed.extendedEnum().lookup(name))
+ .collect(Collectors.toList());
+
+ assertThat(results).hasSize(3);
+ assertThat(results.get(0).getName()).isEqualTo("ThreadStandard");
+ assertThat(results.get(1).getName()).isEqualTo("ThreadMore");
+ assertThat(results.get(2).getName()).isEqualTo("ThreadStandard");
+ }
+
+ /**
+ * Test that ExtendedEnum can be initialized from CompletableFuture with ForkJoinPool.
+ *
+ * CompletableFuture.supplyAsync() by default uses ForkJoinPool.commonPool().
+ */
+ @Test
+ public void test_initialization_in_completable_future() throws Exception {
+ // CompletableFuture.supplyAsync() uses ForkJoinPool.commonPool() by default
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup("ThreadStandard");
+ });
+
+ ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getName()).isEqualTo("ThreadStandard");
+ }
+
+ /**
+ * Test that ExtendedEnum can be initialized from CompletableFuture with explicit ForkJoinPool.
+ */
+ @Test
+ public void test_initialization_in_completable_future_with_custom_pool() throws Exception {
+ ForkJoinPool pool = new ForkJoinPool(2);
+ try {
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup("ThreadMore");
+ }, pool);
+
+ ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getName()).isEqualTo("ThreadMore");
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Test concurrent access from multiple threads in ForkJoinPool.
+ *
+ * This tests that even under high concurrency, ExtendedEnum initialization
+ * works correctly when accessed from ForkJoinPool threads.
+ */
+ @Test
+ public void test_concurrent_access_in_forkjoin_pool() throws Exception {
+ ForkJoinPool pool = new ForkJoinPool(10);
+ try {
+ List> tasks = new ArrayList<>();
+
+ // Create 100 concurrent tasks
+ for (int i = 0; i < 100; i++) {
+ String name = (i % 2 == 0) ? "ThreadStandard" : "ThreadMore";
+ tasks.add(() -> ThreadSafeSampleNamed.extendedEnum().lookup(name));
+ }
+
+ // Submit all tasks and wait for completion
+ List> futures = new ArrayList<>();
+ for (Callable task : tasks) {
+ futures.add(pool.submit(task));
+ }
+
+ // Verify all completed successfully
+ for (Future future : futures) {
+ ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result.getName()).matches("Thread(Standard|More)");
+ }
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(10, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Test that ExtendedEnum initialization works in regular ExecutorService.
+ *
+ * This should work even before the fix, as regular ExecutorService threads
+ * typically have the correct context classloader. This test serves as a
+ * sanity check that the issue is specific to ForkJoinPool.
+ */
+ @Test
+ public void test_initialization_in_executor_service() throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(4);
+ try {
+ Future future = executor.submit(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup("ThreadStandard");
+ });
+
+ ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getName()).isEqualTo("ThreadStandard");
+ } finally {
+ executor.shutdown();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Test using parallel IntStream which also uses ForkJoinPool.
+ */
+ @Test
+ public void test_initialization_in_parallel_int_stream() {
+ List results = IntStream.range(0, 50)
+ .parallel()
+ .mapToObj(i -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup(i % 2 == 0 ? "ThreadStandard" : "ThreadMore");
+ })
+ .collect(Collectors.toList());
+
+ assertThat(results).hasSize(50);
+ assertThat(results).allMatch(item -> item != null);
+ assertThat(results).allMatch(item ->
+ "ThreadStandard".equals(item.getName()) || "ThreadMore".equals(item.getName()));
+ }
+
+ /**
+ * Test simulating the real-world usage pattern from the issue report.
+ *
+ * This simulates bond pricing calculations running in parallel via ForkJoinPool,
+ * where each calculation needs to access ExtendedEnum (e.g., DayCount). This is
+ * the exact usage pattern that triggered the ClassNotFoundException in production.
+ *
+ * Note: Uses ThreadSafeSampleNamed instead of actual DayCount to keep test isolated.
+ */
+ @Test
+ public void test_real_world_scenario_bond_pricing() throws Exception {
+ ForkJoinPool pool = new ForkJoinPool(4);
+ try {
+ // Simulate processing multiple bonds in parallel
+ List bonds = Arrays.asList("Bond1", "Bond2", "Bond3", "Bond4");
+
+ List> futures = new ArrayList<>();
+ for (String bond : bonds) {
+ futures.add(pool.submit(() -> calculateAccruedInterest(bond)));
+ }
+
+ // Verify all calculations completed
+ for (Future future : futures) {
+ String result = future.get(5, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result).contains("DayCount:");
+ }
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Simulates a bond pricing calculation that uses ExtendedEnum.
+ *
+ * When called from ForkJoinPool, this triggers ExtendedEnum initialization in a thread
+ * where the context classloader may be null or inappropriate. In production, this would
+ * be code like: {@code DayCount dayCount = DayCounts.ACT_360;}
+ */
+ private String calculateAccruedInterest(String bondId) {
+ // Simulates the real production code path that caused the issue
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ ThreadSafeSampleNamed dayCount = enumInstance.lookup("ThreadStandard");
+
+ return bondId + " DayCount: " + dayCount.getName();
+ }
+
+ /**
+ * Test that multiple ForkJoinPools can safely initialize different ExtendedEnum instances.
+ */
+ @Test
+ public void test_multiple_pools_multiple_enums() throws Exception {
+ ForkJoinPool pool1 = new ForkJoinPool(2);
+ ForkJoinPool pool2 = new ForkJoinPool(2);
+
+ try {
+ // Pool 1 initializes ThreadSafeSampleNamed
+ Future future1 = pool1.submit(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup("ThreadStandard");
+ });
+
+ // Pool 2 also initializes ThreadSafeSampleNamed
+ Future future2 = pool2.submit(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ return enumInstance.lookup("ThreadMore");
+ });
+
+ // Both should succeed
+ assertThat(future1.get(5, TimeUnit.SECONDS).getName()).isEqualTo("ThreadStandard");
+ assertThat(future2.get(5, TimeUnit.SECONDS).getName()).isEqualTo("ThreadMore");
+ } finally {
+ pool1.shutdown();
+ pool2.shutdown();
+ pool1.awaitTermination(5, TimeUnit.SECONDS);
+ pool2.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Diagnostic test examining the thread context classloader (TCCL) in ForkJoinPool.
+ *
+ * In production environments with complex classloader hierarchies (application servers,
+ * OSGi), ForkJoinPool worker threads often have a null or inappropriate TCCL. This is
+ * the root cause of the ClassNotFoundException reported in issue #2748 when RenameHandler
+ * tries to load classes using the TCCL.
+ *
+ * In this Maven test environment, the TCCL may not be null because all classes share
+ * the same classloader, preventing reproduction of the issue.
+ */
+ @Test
+ public void test_diagnostic_thread_context_classloader() throws Exception {
+ ForkJoinPool pool = new ForkJoinPool(1);
+ try {
+ Future future = pool.submit(() -> Thread.currentThread().getContextClassLoader());
+
+ ClassLoader tccl = future.get(5, TimeUnit.SECONDS);
+
+ // In Maven's test environment, TCCL is typically not null, but in production
+ // application servers and OSGi containers, it often is null or points to the
+ // wrong classloader, causing ClassNotFoundException (issue #2748).
+
+ // Verify ExtendedEnum initialization works in this test environment
+ assertThatCode(() -> {
+ ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ enumInstance.lookup("ThreadStandard");
+ }).doesNotThrowAnyException();
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
+}
+
diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java
new file mode 100644
index 0000000000..990eed0c9c
--- /dev/null
+++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies
+ *
+ * Please see distribution for license.
+ */
+package com.opengamma.strata.collect.named;
+
+/**
+ * Additional mock named object provider for thread safety testing.
+ */
+public class ThreadMoreSampleNameds implements ThreadSafeSampleNamed {
+
+ /**
+ * Another instance.
+ */
+ public static final ThreadMoreSampleNameds MORE = new ThreadMoreSampleNameds();
+
+ @Override
+ public String getName() {
+ return "ThreadMore";
+ }
+
+}
diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java
new file mode 100644
index 0000000000..97cc44e6b8
--- /dev/null
+++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies
+ *
+ * Please see distribution for license.
+ */
+package com.opengamma.strata.collect.named;
+
+/**
+ * Mock named object for thread safety testing.
+ *
+ * This is a separate type from {@link SampleNamed} to ensure we can test
+ * fresh initialization in different threading contexts without interference
+ * from other tests.
+ */
+public interface ThreadSafeSampleNamed extends Named {
+
+ /**
+ * Gets the extended enum helper for thread safety testing.
+ *
+ * @return the extended enum helper
+ */
+ public static ExtendedEnum extendedEnum() {
+ return ExtendedEnum.of(ThreadSafeSampleNamed.class);
+ }
+
+}
+
diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java
new file mode 100644
index 0000000000..6146c7380a
--- /dev/null
+++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies
+ *
+ * Please see distribution for license.
+ */
+package com.opengamma.strata.collect.named;
+
+/**
+ * Mock named object provider for thread safety testing.
+ */
+public class ThreadSafeSampleNameds implements ThreadSafeSampleNamed {
+
+ /**
+ * Standard instance.
+ */
+ public static final ThreadSafeSampleNameds STANDARD = new ThreadSafeSampleNameds();
+
+ @Override
+ public String getName() {
+ return "ThreadStandard";
+ }
+
+}
+
diff --git a/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini b/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini
new file mode 100644
index 0000000000..d51d0dc155
--- /dev/null
+++ b/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini
@@ -0,0 +1,9 @@
+# ExtendedEnum configuration for thread safety testing
+
+[chain]
+chainNextFile = false
+
+[providers]
+com.opengamma.strata.collect.named.ThreadSafeSampleNameds = constants
+com.opengamma.strata.collect.named.ThreadMoreSampleNameds = constants
+