Skip to content

Thread Safety Is a Design Property

Shared mutable state is what makes code unsafe under threads. Break either half — make it unshared with ThreadLocal, or make it immutable — and put the driver behind an accessor before you ever need to.

3 min read

The general rule behind all of this is worth stating plainly, because interviewers ask for it and it applies well beyond drivers.

Shared mutable state is what makes code unsafe under threads. Break either half and you are safe. Make it unshared, which is the ThreadLocal approach and by far the easiest for test frameworks. Or make it immutable, like the Strings in Chapter 14 and the constants in Chapter 13, because a value nobody can change cannot be corrupted by two threads.

So in a parallel suite: the driver goes in a ThreadLocal. Configuration is read once and left unchanged, which is safe because it never changes. Test data belongs to each test rather than to a shared list that tests add to. And a static counter that several tests increment is a defect waiting for the day someone raises the thread count — is static bad, then? No. static is right for constants, for utility methods that hold no state, and for the ThreadLocal holder itself. The danger is a static field whose value changes while tests run. Static and unchanging is fine; static and mutable is the problem. (Locks and synchronized are the tools for genuinely sharing mutable data between threads — a well-designed test suite avoids that need rather than managing it, and knowing they exist is the right depth for an automation role.)

A framework keeps public static WebDriver driver; in BaseTest. The suite is green and runs one test at a time, and it has worked for two years — every page object reaches it with BaseTest.driver, and it is simple. It has worked because it was never run in parallel. The day it is, two tests write to that one field, and one starts driving the other's browser. Fixing it then means it's threaded through every page object already — a rewrite rather than a change. Put it in a ThreadLocal behind a getDriver() method now, while there is one place to change. Everything else keeps calling getDriver() and never knows. Put the driver behind an accessor from day one, even single-threaded. Hiding how the driver is stored is the encapsulation lesson from Chapter 7, and it turns a future rewrite into a one-line change.

This is the standard DriverManager that appears in most professional frameworks: a static final ThreadLocal<WebDriver> with setDriver, getDriver, and unload methods, set called in setup and remove called in teardown. Page objects and tests only ever call getDriver(), so they neither know nor care how it is stored. TestNG and JUnit both run parallel tests on a thread pool, which is why remove() matters — those threads are reused. And the generics from earlier in this chapter are why ThreadLocal<WebDriver> hands you back a WebDriver rather than an Object you must cast.

High-value questions, especially for senior automation roles: why a static WebDriver is unsafe in parallel execution (one field for the whole program, so concurrent tests overwrite each other's driver); what ThreadLocal does (holds a separate value per thread, so each test gets its own driver); why call remove() (frameworks reuse pooled threads, so a stale value reaches the next test and memory is retained); what type erasure is (generic type information is used at compile time and largely removed at run time); what makes code thread-safe (no shared mutable state — make it unshared or immutable); and why use generics at all (compile-time type checking and no casting).

Everything in this chapter comes down to one small, real class. The closing exercise shows it built wrong, one defect per earlier chapter, and then built right.

This article is part of Automation Foundations, in the Automation Engineering track. See the full sequence to find what comes next.