+ + {static} set(context : UserContext) : void
+ + {static} get() : UserContext
+ + {static} clear() : void
+ }
+
+ class RequestHandler {
+ - token : String
+ + RequestHandler(token : String)
+ + process() : void
+ - parseToken(token : String) : Long
+ }
+
+ UserContextProxy --> UserContext : manages
+ RequestHandler --> UserContextProxy : uses static
+ RequestHandler --> UserContext : creates
+ APP --> RequestHandler : creates
+ APP --> Thread : starts
+ Thread --> RequestHandler : executes
+
+}
+@enduml
\ No newline at end of file
diff --git a/thread-specific-storage/pom.xml b/thread-specific-storage/pom.xml
new file mode 100644
index 000000000000..1af91514369b
--- /dev/null
+++ b/thread-specific-storage/pom.xml
@@ -0,0 +1,89 @@
+
+
+
+
+ 4.0.0
+
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+
+ thread-specific-storage
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.2.1
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.threadspecificstorage.App
+
+
+
+
+
+
+
+
+
diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java
new file mode 100644
index 000000000000..a84388acfe08
--- /dev/null
+++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java
@@ -0,0 +1,42 @@
+package com.iluwatar.threadspecificstorage;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Application entry point demonstrating the Thread-Specific Storage pattern.
+ *
+ * This example simulates concurrent request processing for multiple users. Each request carries
+ * a user token, and user-specific context is managed transparently using thread-specific storage.
+ */
+@Slf4j
+public class App {
+
+ /**
+ * Runs the Thread-Specific Storage pattern demonstration.
+ *
+ * @param args command-line arguments (not used)
+ */
+ public static void main(String[] args) {
+ // Simulate concurrent requests from multiple users
+ for (int i = 1; i <= 5; i++) {
+ // Simulate tokens for different users
+ String token = "token::" + (i % 3 + 1); // 3 distinct users
+
+ new Thread(
+ () -> {
+ // Simulate request processing flow
+ RequestHandler handler = new RequestHandler(token);
+ handler.process();
+ })
+ .start();
+
+ // Slightly stagger request times
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.warn("Sleep interrupted", e);
+ }
+ }
+ }
+}
diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java
new file mode 100644
index 000000000000..d5f2cca2830d
--- /dev/null
+++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java
@@ -0,0 +1,57 @@
+package com.iluwatar.threadspecificstorage;
+
+import java.security.SecureRandom;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Application Thread
+ *
+ *
Each instance simulates a request-processing thread that uses the Thread-Specific Object Proxy
+ * to access Thread-Specific Object.
+ */
+@Slf4j
+public class RequestHandler {
+ private final String token;
+
+ public RequestHandler(String token) {
+ this.token = token;
+ }
+
+ /**
+ * Simulated business process: 1. Parse userId from token ("Token::userId"). 2. Store userId in
+ * thread-local storage. 3. Later, retrieve userId and use it for business logic. 4. Finally,
+ * clear thread-local to prevent memory leak.
+ */
+ public void process() {
+ LOGGER.info("Start handling request with token: {}", token);
+
+ try {
+ // Step 1: Parse token to get userId
+ Long userId = parseToken(token);
+
+ // Step 2: Save userId in ThreadLocal storage
+ UserContextProxy.set(new UserContext(userId));
+
+ // Simulate delay between stages of request handling
+ Thread.sleep(200);
+
+ // Step 3: Retrieve userId later in the request flow
+ Long retrievedId = UserContextProxy.get().getUserId();
+ SecureRandom random = new SecureRandom();
+ String accountInfo = retrievedId + "'s account: " + random.nextInt(400);
+ LOGGER.info(accountInfo);
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ // Step 4: Clear ThreadLocal to avoid potential memory leaks
+ UserContextProxy.clear();
+ }
+ }
+
+ private Long parseToken(String token) {
+ // token format: "Token::1234"
+ String[] parts = token.split("::");
+ return (parts.length == 2) ? Long.parseLong(parts[1]) : -1L;
+ }
+}
diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java
new file mode 100644
index 000000000000..51399a1a0c72
--- /dev/null
+++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java
@@ -0,0 +1,15 @@
+package com.iluwatar.threadspecificstorage;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * Thread-Specific Object
+ *
+ *
Provides a service or data that is only accessible via a particular thread.
+ */
+@Data
+@AllArgsConstructor
+public class UserContext {
+ private Long userId;
+}
diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java
new file mode 100644
index 000000000000..468e554b0bf6
--- /dev/null
+++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java
@@ -0,0 +1,33 @@
+package com.iluwatar.threadspecificstorage;
+
+/**
+ * Thread-Specific Object Proxy
+ *
+ *
The Thread-Specific Object Proxy acts as an intermediary, enabling application thread to
+ * access and manipulate thread-specific objects simply and securely.
+ */
+public class UserContextProxy {
+ /**
+ * Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key
+ * for the map.So That there is also no key factory.
+ */
+ private static final ThreadLocal userContextHolder = new ThreadLocal<>();
+
+ /** Private constructor to prevent instantiation of this utility class. */
+ private UserContextProxy() {}
+
+ /** Set UserContext for the current thread. */
+ public static void set(UserContext context) {
+ userContextHolder.set(context);
+ }
+
+ /** Get UserContext for the current thread. */
+ public static UserContext get() {
+ return userContextHolder.get();
+ }
+
+ /** Clear UserContext to prevent potential memory leaks. */
+ public static void clear() {
+ userContextHolder.remove();
+ }
+}
diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java
new file mode 100644
index 000000000000..775ded85b8ea
--- /dev/null
+++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java
@@ -0,0 +1,38 @@
+package com.iluwatar.threadspecificstorage;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import org.junit.jupiter.api.Test;
+
+/** Tests for APP class */
+class AppTest {
+ @Test
+ void testMainMethod() {
+ // Capture system output
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+
+ // Run the main method
+ App.main(new String[] {});
+
+ // Give some time for threads to execute
+ await()
+ .atMost(5, SECONDS)
+ .pollInterval(100, MILLISECONDS)
+ .until(() -> outContent.toString().contains("Start handling request with token"));
+
+ // Verify output contains expected log messages
+ String output = outContent.toString();
+ assertTrue(
+ output.contains("Start handling request with token"),
+ "Should contain request handling start messages");
+
+ // Restore system output
+ System.setOut(System.out);
+ }
+}
diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java
new file mode 100644
index 000000000000..e5f5968e7794
--- /dev/null
+++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java
@@ -0,0 +1,32 @@
+package com.iluwatar.threadspecificstorage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class RequestHandlerTest {
+
+ @Test
+ void process_shouldStoreAndClearUserContext() {
+ // Given - a request handler without proxy parameter
+ RequestHandler handler = new RequestHandler("token::123");
+
+ // When - process the request
+ handler.process();
+
+ // Then - after processing, ThreadLocal should be cleared
+ assertNull(UserContextProxy.get(), "ThreadLocal should be cleared after process()");
+ }
+
+ @Test
+ void process_withInvalidToken_shouldSetUserIdToMinusOne() {
+ // Given - a request handler without proxy parameter
+ RequestHandler handler = new RequestHandler("invalid-token");
+
+ // When - process the request
+ handler.process();
+
+ // Then - after processing, ThreadLocal should be cleared
+ assertNull(UserContextProxy.get(), "ThreadLocal should be cleared even for invalid token");
+ }
+}
diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java
new file mode 100644
index 000000000000..fd8cfbd6b7aa
--- /dev/null
+++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java
@@ -0,0 +1,70 @@
+package com.iluwatar.threadspecificstorage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for UserContextProxy class */
+class UserContextProxyTest {
+
+ private UserContext userContext;
+
+ @BeforeEach
+ void setUp() {
+ userContext = new UserContext(123L);
+ }
+
+ @AfterEach
+ void tearDown() {
+ UserContextProxy.clear();
+ }
+
+ @Test
+ void testSetAndGetContext() {
+ UserContextProxy.set(userContext);
+ UserContext retrievedContext = UserContextProxy.get();
+ assertNotNull(retrievedContext, "Retrieved context should not be null");
+ assertEquals(
+ userContext.getUserId(),
+ retrievedContext.getUserId(),
+ "Retrieved context should have the same userId");
+ }
+
+ @Test
+ void testGetContextWhenNotSet() {
+ UserContext retrievedContext = UserContextProxy.get();
+ assertNull(retrievedContext, "Context should be null when not set");
+ }
+
+ @Test
+ void testClearContext() {
+ UserContextProxy.set(userContext);
+ UserContextProxy.clear();
+ UserContext retrievedContext = UserContextProxy.get();
+ assertNull(retrievedContext, "Context should be null after clearing");
+ }
+
+ @Test
+ void testThreadIsolation() throws InterruptedException {
+ UserContext context1 = new UserContext(123L);
+ UserContext context2 = new UserContext(456L);
+ UserContextProxy.set(context1);
+ // Create another thread to set different context
+ Thread thread =
+ new Thread(
+ () -> {
+ UserContextProxy.set(context2);
+ UserContext threadContext = UserContextProxy.get();
+ assertNotNull(threadContext);
+ assertEquals(456L, threadContext.getUserId());
+ });
+ thread.start();
+ thread.join();
+ // Main thread context should remain unchanged
+ UserContext mainThreadContext = UserContextProxy.get();
+ assertNotNull(mainThreadContext);
+ assertEquals(123L, mainThreadContext.getUserId());
+ }
+}
diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java
new file mode 100644
index 000000000000..3a7864c9e92e
--- /dev/null
+++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java
@@ -0,0 +1,57 @@
+package com.iluwatar.threadspecificstorage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/** Tests for UserContext class */
+class UserContextTest {
+
+ @Test
+ void testConstructorAndGetUserId() {
+ Long userId = 123L;
+ UserContext context = new UserContext(userId);
+
+ assertEquals(
+ userId, context.getUserId(), "UserId should match the one provided in constructor");
+ }
+
+ @Test
+ void testSetUserId() {
+ UserContext context = new UserContext(123L);
+ Long newUserId = 456L;
+
+ context.setUserId(newUserId);
+
+ assertEquals(newUserId, context.getUserId(), "UserId should be updated");
+ }
+
+ @Test
+ void testToString() {
+ Long userId = 123L;
+ UserContext context = new UserContext(userId);
+
+ String expected = "UserContext(userId=" + userId + ")";
+ assertEquals(expected, context.toString(), "toString should return expected format");
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ Long userId = 123L;
+ UserContext context1 = new UserContext(userId);
+ UserContext context2 = new UserContext(userId);
+ UserContext context3 = new UserContext(456L);
+
+ assertEquals(context1, context2, "Objects with same userId should be equal");
+ assertEquals(
+ context1.hashCode(),
+ context2.hashCode(),
+ "Objects with same userId should have same hashCode");
+
+ assertNotEquals(context1, context3, "Objects with different userId should not be equal");
+ assertNotEquals(
+ context1.hashCode(),
+ context3.hashCode(),
+ "Objects with different userId should have different hashCode");
+ }
+}