Factory Pattern in Selenium Java: Stop Writing Gross if-else Blocks
You build a nice, clean Selenium framework. The tests run smoothly on Chrome. Everyone’s happy… until someone says: “Can we run this on Firefox, Edge, Safari, and the Selenium Grid as well?”
If your first thought is “No problem, I’ll just add a few more if-else blocks around new ChromeDriver(),” this article is for you. The Factory Pattern in Selenium Java gives you a better way: a single, extensible place to create browser instances without littering your tests with branching logic.
In this guide, you’ll see how a WebDriver factory turns messy driver setup into a small, elegant abstraction that plays nicely with Page Object Model, supports parallel execution, and keeps your framework open to whatever browser or cloud grid comes next. This is where you stop “just scripting” and start designing.
Why Driver Setup Turns Into an if-else Jungle
Most Selenium frameworks start simple:
WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
Totally fine… until the second requirement arrives:
- “We also need to support Firefox.”
- “We should be able to run headless in CI.”
- “We’re moving to Selenium Grid / cloud providers.”
The typical reaction is to pile on conditions:
public WebDriver initializeDriver(String browser) {
WebDriver driver;
if (browser.equalsIgnoreCase("chrome")) {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
} else if (browser.equalsIgnoreCase("firefox")) {
WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
} else if (browser.equalsIgnoreCase("edge")) {
WebDriverManager.edgedriver().setup();
driver = new EdgeDriver();
} else if (browser.equalsIgnoreCase("chrome-headless")) {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
driver = new ChromeDriver(options);
} else {
throw new IllegalArgumentException("Unknown browser: " + browser);
}
return driver;
}
On the surface this looks “OK”, but it hides several problems:
-
Tight coupling Your setup code knows every concrete driver class (
ChromeDriver,FirefoxDriver,EdgeDriver), plus their options and capabilities. Any change in how you configure a browser ripples through this method. -
Violation of Single Responsibility This method decides what to build (Chrome vs Firefox) and how to build it (drivers, options, capabilities). It’s doing too much.
-
Violation of Open/Closed Principle Every new browser type means editing this very method. Your code is “open for modification” everywhere, instead of “open for extension” via new classes. (BrowserStack)
-
Hard to test and reason about There’s only one heavily branched path through which all driver creation flows. It’s harder to unit test and harder to reuse.
Now add Selenium Grid, multiple environments (dev/qa/stage), or vendor clouds, and the method mutates into a monster. This is the smell that tells you: it’s time for a design pattern.
From if-else Jungle to the Factory Pattern in Selenium Java
In classic design-pattern terms, you have many concrete products (ChromeDriver, FirefoxDriver, RemoteWebDriver with capabilities, etc.) that all implement a common interface (WebDriver). The code that uses these products—your tests, page objects, and utilities—shouldn’t care which concrete class it gets, as long as it behaves like a WebDriver. That’s the sweet spot for the Factory Method pattern. (Medium)
Conceptually:
- The Product is
WebDriver. - The Concrete Products are
ChromeDriver,FirefoxDriver,EdgeDriver,RemoteWebDriver, etc. - The Creator / Factory is a
DriverFactory(orWebDriverManager) class that knows how to build each concrete product. - The Client code (tests, page objects, hooks) asks the factory for a driver instead of instantiating it directly.
So instead of scattering new ChromeDriver() all over your test code, you centralize that logic:
WebDriver driver = DriverFactory.createDriver(BrowserType.CHROME);
Behind this single call, the factory handles:
- Setting up drivers (e.g., using WebDriverManager)
- Configuring options/capabilities
- Deciding between local vs remote/grid
- Injecting things like timeouts, window size, or proxies
Numerous Selenium design-pattern guides recommend exactly this: a dedicated WebDriver factory/manager as the canonical example of Factory pattern in test automation. (BrowserStack)
Designing a Simple WebDriver Factory (Step by Step)
Let’s build a simple but solid WebDriver factory for a typical Java + TestNG setup.
1. Define an enum for browser types
public enum BrowserType {
CHROME,
FIREFOX,
EDGE,
CHROME_HEADLESS
}
You could also read these values from a config file or system properties. An enum keeps things type-safe.
2. Implement the factory class
public final class DriverFactory {
private DriverFactory() {
// prevent instantiation
}
public static WebDriver createDriver(BrowserType browser) {
WebDriver driver;
switch (browser) {
case CHROME:
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
break;
case FIREFOX:
WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
break;
case EDGE:
WebDriverManager.edgedriver().setup();
driver = new EdgeDriver();
break;
case CHROME_HEADLESS:
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
driver = new ChromeDriver(options);
break;
default:
throw new IllegalArgumentException("Unsupported browser: " + browser);
}
driver.manage().window().maximize();
return driver;
}
}
What improved compared to the if-else jungle?
- Tests no longer know about
WebDriverManageror browser-specific options. - There is one single place to add new browsers or tweak configuration.
- Your calling code is clean and expressive:
public class LoginTest {
private WebDriver driver;
@BeforeMethod
public void setUp() {
BrowserType browser = BrowserType
.valueOf(System.getProperty("browser", "CHROME"));
driver = DriverFactory.createDriver(browser);
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void userCanLogin() {
driver.get("https://example.com/login");
// page object calls...
}
}
Your tests now “order a coffee” from the barista instead of grinding the beans themselves. That’s already a win.
Making the WebDriver Factory Parallel-Test Friendly with ThreadLocal
Run your suite in parallel and a new problem appears: shared static drivers.
The bad pattern: Single static driver
public class BadDriverManager {
private static WebDriver driver;
public static WebDriver getDriver() {
if (driver == null) {
driver = DriverFactory.createDriver(BrowserType.CHROME);
}
return driver;
}
}
This works for serial execution but breaks horribly in parallel testing:
- Multiple threads share the same driver instance.
- One test can close the driver while others are still using it.
- Browser state bleeds between tests, causing flaky failures.
Most modern guides on parallel Selenium execution recommend using ThreadLocal<WebDriver> to isolate driver instances per test thread. (testgrid.io)
The good pattern: ThreadLocal driver manager
public final class DriverManager {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
private DriverManager() {}
public static WebDriver getDriver() {
return DRIVER.get();
}
public static void setDriver(WebDriver driver) {
DRIVER.set(driver);
}
public static void quitDriver() {
WebDriver driver = DRIVER.get();
if (driver != null) {
driver.quit();
DRIVER.remove();
}
}
}
Now wire it into your test lifecycle:
public class BaseTest {
@Parameters({"browser"})
@BeforeMethod(alwaysRun = true)
public void setUp(@Optional("CHROME") String browserName) {
BrowserType browser = BrowserType.valueOf(browserName.toUpperCase());
WebDriver driver = DriverFactory.createDriver(browser);
DriverManager.setDriver(driver);
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
DriverManager.quitDriver();
}
protected WebDriver driver() {
return DriverManager.getDriver();
}
}
A test then looks like:
public class CheckoutTest extends BaseTest {
@Test
public void userCanCheckoutWithVisa() {
driver().get("https://example.com/");
// ...
}
}
Each parallel test thread gets:
- Its own
WebDriverinstance. - Its own browser session.
- Independent clean-up.
This pattern scales nicely whether you’re running on local browsers or a Selenium Grid.
Extending the Factory for Remote, Grid, and Cloud Providers
Most teams eventually need to run on:
- Selenium Grid (on-prem)
- Docker-based grids
- Cloud providers (BrowserStack, Sauce Labs, LambdaTest, etc.)
Instead of sprinkling RemoteWebDriver logic everywhere, extend the factory.
1. Add an execution type enum
public enum ExecutionType {
LOCAL,
REMOTE
}
2. Enhance the factory
public final class DriverFactory {
private static final String GRID_URL =
System.getProperty("gridUrl", "http://localhost:4444/wd/hub");
private DriverFactory() {}
public static WebDriver createDriver(BrowserType browser) {
ExecutionType executionType = ExecutionType.valueOf(
System.getProperty("executionType", "LOCAL").toUpperCase()
);
switch (executionType) {
case REMOTE:
return createRemoteDriver(browser);
case LOCAL:
default:
return createLocalDriver(browser);
}
}
private static WebDriver createLocalDriver(BrowserType browser) {
// same as earlier simple factory: ChromeDriver, FirefoxDriver, etc.
// ...
}
private static WebDriver createRemoteDriver(BrowserType browser) {
MutableCapabilities capabilities;
switch (browser) {
case CHROME:
case CHROME_HEADLESS:
ChromeOptions chromeOptions = new ChromeOptions();
if (browser == BrowserType.CHROME_HEADLESS) {
chromeOptions.addArguments("--headless=new");
}
capabilities = chromeOptions;
break;
case FIREFOX:
capabilities = new FirefoxOptions();
break;
case EDGE:
capabilities = new EdgeOptions();
break;
default:
throw new IllegalArgumentException("Unsupported browser: " + browser);
}
try {
return new RemoteWebDriver(new URL(GRID_URL), capabilities);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid GRID_URL: " + GRID_URL, e);
}
}
}
Now your test setup doesn’t change at all. Only your configuration does:
- Local Chrome:
-Dbrowser=CHROME -DexecutionType=LOCAL - Headless Chrome in CI:
-Dbrowser=CHROME_HEADLESS -DexecutionType=LOCAL - Grid Firefox:
-Dbrowser=FIREFOX -DexecutionType=REMOTE -DgridUrl=http://grid:4444/wd/hub
Cloud vendors often share similar examples, showing a WebDriver factory that encapsulates all the remote capability building, including things like OS version, resolution, and build name. (Elias Nogueira)
Common Mistakes When Implementing a WebDriver Factory
Even with the right pattern, it’s easy to stumble into subtle problems.
1. Mixing test logic into the factory
Bad:
public static WebDriver createDriver(BrowserType browser) {
WebDriver driver = // ...
driver.get("https://app-under-test.com");
loginAsAdmin(driver);
return driver;
}
Now your factory:
- Knows application URLs.
- Knows business flows (“login as admin”).
Keep the factory focused on creating and configuring drivers. App flows belong in page objects and test setup utilities.
2. Not cleaning up per thread
If you set a ThreadLocal driver and forget to call quitDriver() and remove(), you can leak sessions and memory, especially in long-running suites.
3. Overusing static global access
Having a static DriverManager.getDriver() is convenient, but treat it as an infrastructure layer, not a magic global. Passing the driver into page objects explicitly still leads to better testability and separation of concerns.
4. Letting the factory grow into a “god class”
If your DriverFactory gains dozens of case branches and complex logic unique to certain browsers, it might be time to refactor into smaller components—e.g. specialized “manager” classes per browser type, each implementing a small interface the factory calls. (BrowserStack)
WebDriver Creation Approaches Compared
Here’s a quick comparison of common ways teams manage WebDriver instances.
Hardcoded new ChromeDriver()
- When You See It: Tiny demos, PoCs
- Pros: Simple, obvious
- Cons: No cross-browser, logic duplicated everywhere
Big if-else / switch block
- When You See It: Early frameworks, “one setup to rule all”
- Pros: Centralized, supports multiple browsers
- Cons: Violates OCP, grows endlessly, hard to test
Simple static factory
- When You See It: Small–medium frameworks
- Pros: Single entry point, cleaner tests
- Cons: Not parallel-safe unless combined with ThreadLocal
ThreadLocal + factory
- When You See It: Mature, parallel frameworks
- Pros: Parallel-safe, clean API, extensible
- Cons: Slightly more complex; requires disciplined cleanup
DI container / framework-managed
- When You See It: Very large or Spring/Guice-based suites
- Pros: Strong testability, lifecycle control
- Cons: Higher setup cost; learning curve for the team
Many public tutorials and blog posts settle on “ThreadLocal + WebDriver factory” as a sweet spot between simplicity and power. (testgrid.io)
Best Practices Summary
-
Centralize driver creation. All
new ChromeDriver()calls should live in your factory, not in tests or page objects. -
Use enums or config to control behavior. Model browsers (
BrowserType) and execution type (ExecutionType) explicitly instead of using string literals everywhere. -
Keep factories focused. A WebDriver factory should only configure and create drivers—no URLs, logins, or page flows.
-
Make it parallel-safe with
ThreadLocal. Give each test thread its own driver instance and clean it up reliably in@AfterMethod/@AfterEach. -
Integrate with Page Object Model. Pass the
WebDriverfrom your factory into page objects so they remain decoupled from driver creation. -
Honor SOLID principles.
- SRP: Factory creates drivers; tests orchestrate flows; pages encapsulate UI.
- OCP: Add new browsers by extending the factory, not rewriting test code.
-
Externalize configuration. Read browser, execution type, grid URL, and capabilities from properties/ENV variables so you can switch setups without code changes.
-
Test the factory itself. Add unit/integration tests that verify correct driver types and capabilities for different inputs (e.g. CHROME vs FIREFOX, LOCAL vs REMOTE).
-
Document the contract. Make it clear for your team: “Always obtain drivers via
DriverManager.getDriver()or via dependency injection, never instantiate drivers directly.”
FAQ
1. Is the Factory Pattern overkill for small Selenium projects?
If you’re 100% sure your suite will only ever run on a single local browser, a full-blown factory might feel heavy. But requirements tend to grow: headless CI runs, another browser, or a grid. Starting with a simple factory keeps the door open for change and costs very little upfront.
2. How is a WebDriver factory different from Selenium’s PageFactory?
They sound similar but solve completely different problems:
- A WebDriver factory is a design pattern you implement to create browser instances (
WebDriverobjects). - Selenium’s
PageFactoryis a utility that initializes@FindBy-annotatedWebElementfields in page objects.
Think “factory creates browsers, PageFactory wires web elements.”
3. How do I make my WebDriver factory work with Page Object Model?
Typical flow:
-
Test gets the driver from the factory (directly or via
DriverManager). -
Test creates page objects, passing the driver into their constructors:
LoginPage loginPage = new LoginPage(driver()); -
Page objects use the driver but never create it. This clean separation keeps POM focused purely on UI interactions.
4. How can I avoid my factory class becoming huge?
Extract responsibilities as they grow:
LocalDriverFactoryfor local browsers.RemoteDriverFactoryfor grid/cloud.- Per-browser builders like
ChromeManager,FirefoxManagerthat know how to configure options.
Your main DriverFactory then just coordinates and delegates, keeping its code small and readable. (Elias Nogueira)
5. Should I use a singleton for WebDriver?
A global singleton WebDriver instance is almost always a bad idea:
- It breaks parallel execution.
- It couples test state across test methods and classes.
- It makes cleanup and state isolation difficult.
Prefer one driver per test or per test class, managed through a ThreadLocal-friendly factory or through your test framework’s lifecycle hooks.
Conclusion
The ugly if-else block that creates browsers is more than just an eyesore—it’s a design smell that makes your framework brittle, hard to extend, and dangerous under parallel execution. Moving to the Factory Pattern in Selenium Java gives you:
- A single, well-defined place to configure and create browsers.
- Clean, focused test code that cares about test logic, not driver plumbing.
- Built-in scalability for new browsers, execution environments, and grids without rewriting your suite.
Take a look at your current driver initialization today. If you see duplicated new ChromeDriver() calls or a giant conditional block, that’s your refactoring opportunity. Introduce a WebDriver factory, wire it with ThreadLocal, and enjoy a framework that’s cleaner, more maintainable, and ready for whatever the next browser requirement throws at it.
Sensei Omar Alaa is a Senior QA & Test Automation Engineer with 4+ years of experience in the fintech domain, specialized in Java, Selenium, BDD, TestNG, and API testing, with strong experience leading testing teams and ensuring high-quality releases.
His journey started in manual testing, then he moved deeper into automation—building and enhancing Selenium-based frameworks, and even applying OCR to automate CAPTCHA within regression workflows.
After mentoring and training testers, Omar founded Quality Sensei to deliver practical, structured testing education through hands-on labs and real-world scenarios.
Areas of Expertise:
- Selenium WebDriver (Java) & Automation Frameworks
- BDD, Test Design, and Release Sign-off Quality
- API Testing (Postman, Rest Assured)
- Performance Testing (JMeter – basic)
- Team Leadership, Mentorship & QA Process Improvement