When many engineers start building test automation frameworks, they are introduced to the Page Object Model (POM) almost immediately.
Usually, the introduction looks something like this:
"Create a pages package, create a tests package, put your locators in the pages, and write your test methods."
While that advice gets you started, it often misses the core architectural point.
Over years of designing test frameworks, I have learned that POM is not merely a directory convention—it is an encapsulation pattern. Understanding the why behind its boundaries is what separates a brittle, flaky framework from one that scales effortlessly.
Here is my mental model for designing clean, maintainable automation architecture from first principles.
1. What Goes Where: Framework Organization vs. POM
A common misconception is believing that creating folders such as pages, tests, utils, and base means you have implemented POM.
POM specifically means that page-specific locators and actions are encapsulated inside Page Object classes.
The folder structure is simply framework organization built around POM:
Automation Framework
│
├── src/test/java
│ ├── pages
│ │ ├── LoginPage.java
│ │ ├── HomePage.java
│ │ └── DashboardPage.java
│ │
│ ├── tests
│ │ ├── LoginTest.java
│ │ └── DashboardTest.java
│ │
│ ├── utils
│ │ ├── DriverManager.java
│ │ └── ConfigReader.java
│ │
│ └── base
│ └── BaseTest.java
│
└── resources
└── config.properties
The Responsibilities:
pages(Page Objects): Each application page gets its own dedicated class containing its locators and page-specific actions.tests(Test Cases): Tests interact with Page Objects rather than raw web elements. The test focuses on what is being tested; the Page Object handles how the UI is driven.base(BaseTest): Shared test lifecycle setup: browser initialization, driver configuration, and teardown.utils(Utilities): Cross-cutting concerns: driver management, configuration reading, screenshot capture, explicit wait wrappers, and test data handlers.
2. The Core Flow: HOW vs. WHAT
At the center of POM is a strict separation of concerns:
Test (WHAT to validate)
│
▼
Page Object (HOW to interact)
│
┌────────┴────────┐
▼ ▼
Locators Actions
│ │
└────────┬────────┘
▼
Browser
The Mental Model:
Page Object = HOW (Locators & UI actions)
Test Class = WHAT (Scenario steps & Assertions)
The Page Object should never care about test assertions or pass/fail conditions. Its sole responsibility is modeling the behavior of the page.
In a LoginPage, you keep the username, password, and login button locators along with methods like login(). In the LoginTest, you call login() and validate whether the transition or dashboard loaded as expected.
3. Why By Locators, and Why Make Them private?
A recurring question in framework design is how to define elements inside a Page Object:
public class LoginPage {
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginButton = By.cssSelector("button[type='submit']");
// ...
}
Why prefer By locators over immediate WebElement instances, and why make them private?
private → Encapsulation (Access control)
By → Locator definition (Strategy)
Page Object → Owns locators + actions
Test Class → Uses actions + performs validations
Key Principles:
Byrepresents locator strategy: It defines how to find an element, allowing the driver to look it up at the exact moment an action is executed.privateguarantees encapsulation: Locators are internal implementation details of the page. Tests should never reach in and touch locators directly. If a developer changes an element ID in the UI, you only update the private locator in one place—no test code breaks.- Encapsulation is access control, not memory saving: Making locators
privateisn't about memory optimization; it's about establishing clear architectural boundaries.
4. Handling Shared Components (Header, Nav, Footer)
Applications rarely consist of isolated pages. Most share headers, side navigation bars, or footers.
Duplicating header locators across HomePage, DashboardPage, and SettingsPage violates DRY (Don't Repeat Yourself) and creates maintenance debt.
Instead, extract shared UI into reusable component classes:
Test
│
▼
Page Object
│
└── Common Component
├── Shared locators
└── Shared actions
Prefer the flow Test → Page Object → Common Component rather than having tests interact with global components directly. When the navigation bar changes, only that shared component is touched.
5. Scaling Without Creating "God Classes"
As a test suite expands to hundreds of scenarios, frameworks risk collapsing under their own weight. The most common trap is the God Class—a bloated BasePage that accumulates every wait, helper, click, and assertion in the repository.
To keep POM maintainable at scale, use tiered responsibilities:
Page Object
↓
Base Page → Common page-level behavior (driver reference, generic waits)
Page Object
↓
Common Component → Reusable UI modules (headers, navigation, modals)
Any framework layer
↓
Utilities → Cross-cutting helpers (config, data setup, reporters)
Before writing a new method, establish a team habit of checking whether reusable functionality already exists. Large test suites should also be segmented early into targeted execution suites (e.g., Smoke vs. Regression).
6. POM vs. Page Factory: A Vital Distinction
One of the most frequent confusions is treating POM and Page Factory as synonyms:
Page Object Model (POM) is a design pattern for organizing UI automation code.
Page Factory is a Selenium-supported mechanism that initializes elements in Page Objects using annotations like@FindBy.
Explicit By Locator:
private By username = By.id("username");
Page Factory Approach:
@FindBy(id = "username")
private WebElement username;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
Trade-offs to Understand:
- POM does not require Page Factory.
- While
@FindBycan look concise, Page Factory relies on proxies. Those proxies do not eliminate synchronization requirements or preventStaleElementReferenceException. - Explicit
Bylocators make the lookup point clear and give you deliberate control over wait strategies. For new frameworks, explicitBylocators are often the cleaner, more predictable choice.
7. POM + BDD: Where Does the Assertion Belong?
When pairing POM with BDD frameworks like Cucumber or Reqnroll:
Scenario: Successful Login
Given I am on the login page
When I login with valid credentials
Then I should see the dashboard
Where should the assertion live?
// DashboardPage.java — HOW (Query method)
public boolean isDashboardDisplayed() {
return driver.findElement(dashboardHeader).isDisplayed();
}
// StepDefinitions.java — WHAT (Assertion)
@Then("I should see the dashboard")
public void verifyDashboard() {
Assert.assertTrue(dashboardPage.isDashboardDisplayed(),
"Dashboard was not displayed after login.");
}
The rule remains clean and intact: Page Object queries the state (HOW), Step Definition validates the expectation (WHAT).
8. Inheriting a Legacy Framework: The Lead SDET Playbook
When taking ownership of a poorly structured framework, the natural instinct is to propose a complete rewrite from scratch.
Experienced engineering leaders avoid immediate rewrites. Instead, follow a disciplined sequence:
Understand the framework
↓
Identify pain points
↓
Prioritize by impact
↓
Implement targeted improvements
↓
Measure reliability and maintainability
- Assess First: Identify the genuine root causes of friction—scattered locators, missing waits, flaky tests, poor test data management, or slow execution.
- Prioritize by Impact: Address what provides the fastest feedback loop and biggest stability gain (e.g., establishing central POM boundaries, stabilizing flaky locators).
- Iterate Incrementally: Refactor page by page, tracking metrics like test pass rates, regression runtimes, and maintenance overhead.
Conclusion
Design patterns in automation exist for one primary reason: to protect test code from UI volatility.
When your Page Objects cleanly own the how, your tests strictly focus on the what, and your architecture separates pages from cross-cutting utilities, your automation becomes what it was always intended to be—a reliable, high-confidence safety net for product delivery.