# How to Use the DriverManager ThreadLocal Pattern for Parallel Test Execution in Selenium

> Master parallel test execution in Selenium with the DriverManager ThreadLocal pattern. Learn how this technique prevents race conditions and isolates WebDriver instances for efficient automation. anhtester automation.

- Repository: [Anh Tester/automationframeworkselenium](https://github.com/anhtester/automationframeworkselenium)
- Tags: how-to-guide
- Published: 2026-02-24

---

**The DriverManager ThreadLocal pattern isolates WebDriver instances per thread by storing them in a ThreadLocal container, enabling thread-safe parallel test execution without race conditions.**

The anhtester/automationframeworkselenium repository demonstrates a production-ready implementation of the DriverManager ThreadLocal pattern for Selenium WebDriver. This architecture ensures that when TestNG executes test methods in parallel, each thread maintains its own isolated browser session, preventing the race conditions and stale element exceptions that occur with shared static WebDriver instances.

## Why ThreadLocal is Essential for Parallel Execution

When TestNG runs tests with `parallel="methods"`, it spawns multiple Java threads simultaneously. A plain static `WebDriver` field would be shared across all threads, leading to race conditions, session collisions, and `StaleElementReferenceException` errors. The **ThreadLocal** class solves this by binding a driver instance to the specific thread that created it.

In [`src/main/java/com/anhtester/driver/DriverManager.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/src/main/java/com/anhtester/driver/DriverManager.java), the framework stores the WebDriver in a `ThreadLocal<WebDriver>` container. Any call to `DriverManager.getDriver()` from within a test method automatically retrieves the instance associated with the current thread, ensuring complete isolation between parallel tests.

## Core Architecture Components

### DriverManager.java

The `DriverManager` class in [`src/main/java/com/anhtester/driver/DriverManager.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/src/main/java/com/anhtester/driver/DriverManager.java) serves as the central registry for WebDriver instances. It maintains a private static `ThreadLocal<WebDriver>` field and exposes three critical static methods:

- **`setDriver(WebDriver driver)`** – Stores the driver instance into the ThreadLocal for the current thread
- **`getDriver()`** – Retrieves the thread-bound driver instance
- **`quit()`** – Removes the driver from ThreadLocal and closes the browser session

This design pattern ensures that `DriverManager.getDriver()` always returns the correct driver for the thread executing the test, regardless of how many tests run simultaneously.

### BaseTest.java

The `BaseTest` class in [`src/test/java/com/anhtester/common/BaseTest.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/src/test/java/com/anhtester/common/BaseTest.java) manages the driver lifecycle for every test method. It uses `@BeforeMethod` and `@AfterMethod` annotations to create and destroy drivers:

1. **Before each test**: Calls `TargetFactory.createInstance()` to build a fresh driver, wraps it with Selenium's `ThreadGuard.protect()` for additional thread safety, and registers it via `DriverManager.setDriver()`
2. **After each test**: Invokes `DriverManager.quit()` to clean up the thread-local session

This guarantees that each test method receives a pristine browser instance while maintaining thread isolation.

### TargetFactory.java

Located in [`src/main/java/com/anhtester/driver/TargetFactory.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/src/main/java/com/anhtester/driver/TargetFactory.java), the `TargetFactory` encapsulates driver instantiation logic. The `createInstance(String browser)` method determines whether to create a local driver via `BrowserFactory` or a remote driver for Selenium Grid/Docker execution. It returns a fresh `WebDriver` instance that `BaseTest` immediately binds to the current thread.

## Configuring TestNG for Parallel Execution

To leverage the ThreadLocal pattern, configure TestNG to execute methods in parallel. The [`testng.xml`](https://github.com/anhtester/automationframeworkselenium/blob/main/testng.xml) configuration specifies the parallel mode and thread count:

```xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="ParallelSuite" parallel="methods" thread-count="4">
    <test name="CRMTests">
        <classes>
            <class name="com.anhtester.projects.crm.testcases.LoginTest"/>
            <class name="com.anhtester.projects.crm.testcases.CreateContactTest"/>
        </classes>
    </test>
</suite>

```

With `parallel="methods"` and `thread-count="4"`, TestNG distributes test methods across four threads. Because each thread maintains its own driver in the ThreadLocal storage, tests execute concurrently without cross-thread contamination.

## Implementing Thread-Safe Test Classes

### Extending BaseTest for Automatic Driver Management

Test classes inherit thread-safe driver handling by extending `BaseTest`. All page objects and utility classes access the driver through `DriverManager.getDriver()`, which resolves to the thread-local instance:

```java
package com.anhtester.projects.crm.testcases;

import org.openqa.selenium.By;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.anhtester.common.BaseTest;
import com.anhtester.driver.DriverManager;

public class LoginTest extends BaseTest {

    @Test
    public void verifyLogin() {
        DriverManager.getDriver().get("https://example-crm.com/login");
        
        DriverManager.getDriver()
                    .findElement(By.id("username"))
                    .sendKeys("admin");
        DriverManager.getDriver()
                    .findElement(By.id("password"))
                    .sendKeys("admin123");
        DriverManager.getDriver()
                    .findElement(By.id("loginBtn"))
                    .click();

        String greeting = DriverManager.getDriver()
                                       .findElement(By.id("welcome"))
                                       .getText();
        Assert.assertTrue(greeting.contains("Welcome"));
    }
}

```

The test inherits driver creation and cleanup logic from `BaseTest`, while `DriverManager` ensures that `getDriver()` returns the correct browser instance for the current thread.

### Accessing Drivers in TestNG Listeners

The ThreadLocal pattern extends to TestNG listeners running in the same thread as the test. In [`src/test/java/com/anhtester/listeners/AllureListener.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/src/test/java/com/anhtester/listeners/AllureListener.java), the listener captures screenshots on failure by accessing the thread-local driver:

```java
public class AllureListener implements ITestListener {
    @Override
    public void onTestFailure(ITestResult result) {
        if (DriverManager.getDriver() != null) {
            byte[] screenshot = ((TakesScreenshot) DriverManager.getDriver())
                                .getScreenshotAs(OutputType.BYTES);
            Allure.addAttachment(result.getName() + "_Failed", 
                                new ByteArrayInputStream(screenshot));
        }
    }
}

```

Since the listener executes in the same thread as the failing test method, `DriverManager.getDriver()` correctly resolves to that test's browser instance.

## Summary

- **ThreadLocal isolation**: The `DriverManager` class in [`DriverManager.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/DriverManager.java) uses `ThreadLocal<WebDriver>` to bind driver instances to specific threads, preventing race conditions during parallel execution.
- **Lifecycle management**: [`BaseTest.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/BaseTest.java) handles driver creation via `TargetFactory` and registration through `DriverManager.setDriver()`, ensuring each test method receives a fresh, thread-bound browser.
- **Concurrent safety**: TestNG's `parallel="methods"` configuration combined with ThreadLocal storage allows multiple tests to run simultaneously without session collisions or stale element errors.
- **Universal access**: Page objects, utilities, and listeners throughout the codebase access the correct driver via `DriverManager.getDriver()`, which automatically resolves to the current thread's instance.

## Frequently Asked Questions

### How does ThreadLocal prevent race conditions in parallel test execution?

`ThreadLocal` creates a separate variable copy for each thread accessing it. When TestNG runs tests in parallel, each thread calls `DriverManager.setDriver()` to store its own WebDriver instance. Subsequent calls to `DriverManager.getDriver()` return only that thread's specific driver, eliminating the possibility of one thread closing or interacting with another thread's browser session.

### What is the role of ThreadGuard in the BaseTest implementation?

`ThreadGuard.protect()`, called in [`BaseTest.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/BaseTest.java) before registering the driver, wraps the WebDriver instance with a proxy that verifies all driver interactions occur on the creating thread. This adds an extra layer of safety beyond ThreadLocal, throwing an exception if code attempts to use the driver from a different thread, which helps catch threading bugs during development.

### Can this pattern support both local and remote WebDriver instances?

Yes. The [`TargetFactory.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/TargetFactory.java) class encapsulates the logic for creating both local drivers through `BrowserFactory` and remote drivers for Selenium Grid or Docker. Regardless of whether the driver runs locally or remotely, `BaseTest` stores the returned WebDriver in the ThreadLocal container via `DriverManager.setDriver()`, maintaining the same thread isolation guarantees.

### How do you clean up WebDriver instances after parallel tests complete?

The `DriverManager.quit()` method removes the driver from the ThreadLocal map and calls `driver.quit()` to close the browser. In [`BaseTest.java`](https://github.com/anhtester/automationframeworkselenium/blob/main/BaseTest.java), this method is invoked in an `@AfterMethod` block, ensuring that each thread's driver is destroyed immediately after its associated test method finishes, freeing up system resources and preventing memory leaks in long-running parallel suites.