Sitemap

The Layered Selenium Architecture That Scaled to 1000+ Tests

6 min readJun 25, 2026

--

Press enter or click to view image in full size

How a structured framework design kept a large automation suite maintainable — and what I learned working on one in production

When most people start writing Selenium tests, they learn about the Page Object Model (POM) and think that’s the whole story.

It isn’t.

In a production project I worked on — a large B2B SaaS platform with multiple user portals — our Selenium suite grew to 1000+ test classes, 20 nightly regression shards on CI, and coverage across two separate web portals. A basic POM would have collapsed under that weight.

What kept it standing was a layered architecture that most tutorials never talk about.

This is a breakdown of exactly how it worked.

The Problem With Basic POM

Standard Page Object Model looks like this:

Test → Page Object → WebDriver

Your test calls a page method. The page interacts with the browser. Simple.

But as a codebase grows, this breaks down fast:

  • Tests start copying the same 5-step login flow everywhere
  • Page objects become 2,000-line monsters
  • Changing one UI element breaks 40 test files
  • New team members have no idea where business logic lives

We needed something more structured. So the framework used clearly separated layers.

The Layered Architecture

┌─────────────────────────────────┐
│ TEST LAYER │ ← What we're testing
│ LoginTest.java │
├─────────────────────────────────┤
│ FUNCTIONS LAYER │ ← How the business works
│ Login.java, Customer.java │
├─────────────────────────────────┤
│ PAGES LAYER │ ← Where things are on screen
│ LoginPage.java, CustomersPage │
├─────────────────────────────────┤
│ KEYWORDBASE LAYER │ ← How to talk to the browser
│ KeywordBase.java │
└─────────────────────────────────┘

Each layer has one job. Let me walk through each one.

Layer 1: KeywordBase — The Browser Translator

This is the foundation. Think of it as a custom DSL (Domain Specific Language) built on top of raw Selenium WebDriver.

Instead of writing this in every test:

WebElement element = wait.until(
ExpectedConditions.elementToBeClickable(locator)
);
element.click();

You write:

ui.click(btn_signIn);

The KeywordBase handles waits, logging, and fallback strategies internally. A well-designed click() method might:

  1. Wait for the element to be clickable via WebDriverWait
  2. Try a standard Selenium click
  3. Fall back to a JavaScript click if standard click fails
  4. Log the failure with locator details for debugging

Beyond basic actions, you can build specialized helpers for real-world problems — methods for infinite-scroll pages, table sorting validation, file download verification, and multi-tab management.

Why this matters: Every growing test suite ends up building this layer eventually. The difference is whether you do it intentionally in one place, or accidentally scattered across hundreds of test files.

Layer 2: Pages — The Screen Map

This is standard POM territory, but kept deliberately thin. Each screen maps to a *Page.java class with two responsibilities only:

public class LoginPage extends TestBase {
// 1. Locators — where things live on screen
By emailField = By.xpath("//input[@placeholder='Email']");
By passwordField = By.xpath("//input[@placeholder='Password']");
By submitButton = By.xpath("//button[@type='submit']");
    // 2. Low-level actions — how to interact with them
public void typeEmail(String value) {
ui.sendKeys(emailField, value);
}
public void clickSubmit() {
ui.click(submitButton);
}
}

No business logic. No assertions. Just screen interaction.

When a locator breaks because the UI changed, you fix it in exactly one place — the page class. Not in 40 test files.

Layer 3: Functions — The Business Workflow Layer

This is the layer most frameworks skip — and it’s the one that made the biggest difference at scale.

The Functions layer exposes static, business-oriented APIs representing real user workflows:

public class Login {
public static void loginAsUser(String email, String password) {
loginPage.typeEmail(email);
loginPage.typePassword(password);
loginPage.clickSubmit();
        // Handle post-login conditional steps
// (e.g. account/company selection that appears for certain user types)
if (!Dashboard.isDefaultAccountSelected()) {
Dashboard.selectDefaultAccount();
}
}
}

Notice what happened. The login flow isn’t just “type, type, click.” In real applications, post-login flows often have conditionals — company selection, onboarding prompts, role-based redirects. That conditional logic lives in the Functions layer, not copy-pasted into every test that needs to log in.

With this in place, tests read like plain English:

Login.loginAsUser(user.getEmail(), user.getPassword());
Dashboard.navigateToCustomers();
Customer.searchByCode(customerId);
Customer.openOrderGuide(customerId);

A new QE joining the team can read that test and understand exactly what it does — no Selenium knowledge required.

Putting It Together: A Real E2E Test

Here’s what a cross-portal End-to-End test looks like using this layered approach:

@Test
public void placeOrderAndVerifyOnSecondPortal() {
// Portal A — perform the action
Login.loginAsUser(user.getEmail(), user.getPassword());
Dashboard.navigateToOrders();
Order.createNewOrder(orderId);
Order.submitOrder();
    // Switch portals — verify the result
Browser.switchToNewTab();
Login.loginAsSecondUser(adminEmail, adminPassword);
Dashboard.navigateToOrderHistory();
OrderHistory.searchByOrderId(orderId);
OrderHistory.verifyOrderStatus("Submitted");
}

That test spans two separate portals, opens a new browser tab mid-flow, and validates a full workflow. The test itself is clean and readable. No raw Selenium. No locators. Just business steps.

How This Scales to 1000+ Tests

The architecture alone isn’t enough. You also need operational structure.

Organize tests by domain, not by type:

tests/
├── order_management/ ← all tests related to orders
├── catalog/ ← all tests related to product catalog
├── payments/ ← all tests related to billing
├── user_management/ ← all tests related to accounts
└── e2e/ ← full cross-feature flows

Each folder maps to a product area. When a feature breaks, you know exactly where to look.

Shard by CI suites:

<!-- bvt.xml — smoke suite, runs on every build -->
<!-- regression1.xml through regression20.xml — nightly shards -->

Splitting 1000+ tests across parallel CI jobs brings nightly run time down to a manageable window. Each job runs independently — failures in one shard don’t block others.

Link automation to manual test cases:

@Test(groups = "TC-28")
public void loginTest() { ... }

Using TestNG groups to store test case IDs creates a direct link between your automated tests and your test management system. After each run, you know exactly which manual scenarios are covered and which failed.

What This Architecture Gets Right

Looking at this framework pattern honestly:

Onboarding is fast. New QEs can write tests using the Functions layer without understanding Selenium internals.

Locator changes are contained. UI refactors that would break hundreds of tests in a flat framework break one page class.

CI sharding works at scale. Parallel suite execution is operationally mature for large teams.

Tests are readable. Business stakeholders can roughly follow what a test does just by reading it.

If I Were Building This From Scratch Today

The layered architecture pattern is solid — but if I were starting a new framework today, here’s what industry best practices say to do differently:

Use ThreadLocal for WebDriver. A static WebDriver instance blocks true method-level parallel execution. ThreadLocal<WebDriver> gives each thread its own isolated driver instance — safe for parallel runs.

// Modern approach
private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
public static WebDriver getDriver() {
return driver.get();
}

Zero implicit wait — explicit conditions only. Mixing implicit waits with WebDriverWait causes unpredictable timing issues. The current best practice is setting implicit wait to zero and using explicit ExpectedConditions everywhere.

// Avoid this
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
// Prefer this
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until(ExpectedConditions.elementToBeClickable(locator));

Fail fast in your KeywordBase. When wrapper methods silently catch exceptions and continue, tests can appear to pass while steps are silently no-op-ing. Let failures surface immediately — your CI pipeline will thank you.

Prioritize stable locators. XPath tied to placeholder text or CSS class names breaks frequently. Modern locator priority looks like this:

data-testid  →  ARIA role/label  →  stable CSS  →  XPath (last resort)

Partnering with your dev team to add data-testid attributes to key elements is one of the highest-ROI investments a QE can make.

Key Takeaways

If you’re building or scaling a Selenium framework, here’s what this architecture teaches:

  1. POM alone doesn’t scale — you need a workflow layer above pages
  2. Build your KeywordBase intentionally — not as an afterthought
  3. Keep layers pure — pages don’t have business logic, functions don’t have locators
  4. Organize by product domain — not by test type
  5. Design for CI from day one — parallel sharding should be possible from the start

What’s Next

In the next article, I’ll go deeper into the gaps I mentioned — specifically 5 Selenium anti-patterns that appear in production frameworks and what the modern alternatives look like, with before/after code comparisons.

After that: the Selenium → Playwright migration story.

Working on a large Selenium framework? Recognized any of these patterns — good or bad? Drop a comment, I’d love to hear how other teams have solved this.

--

--

Viranga Bandara
Viranga Bandara

Written by Viranga Bandara

Former Software Quality Engineer @ Cut+Dry | Manual & Automation Testing | Playwright | Selenium | BSc (Hons) IT – University of Moratuwa