Page Object Model in Selenium and Page Factory: A Practical Guide
If you've ever tried to maintain a growing Selenium test suite, you've probably felt the pain of brittle locators, duplicated code, and tests that randomly fail when the UI changes. This is exactly where the Page Object Model in Selenium becomes essential.
POM helps you model each page (or part of a page) as a class with its own locators and methods, so your tests interact with pages through a clear API instead of scattered findElement calls. (Selenium) Page Factory builds on top of that idea and adds annotations and lazy initialization to make element management even cleaner.
In this guide, we'll walk through what Page Object Model and Page Factory are, how they work together, and how you can apply them in real-world projects. We'll use Java + Selenium examples, highlight common mistakes, and finish with best practices you can adopt in your current framework today.
Why You Need the Page Object Model in Selenium
As your UI test suite grows, you quickly run into a few classic problems:
- The same locators are copied across dozens of tests.
- A small UI change (like a new ID) breaks a whole batch of tests.
- New team members struggle to understand what your tests are really doing.
The Page Object Model in Selenium addresses this by treating each page as an object-oriented class that serves as an interface to that page: locators and UI interactions live inside the page class, while assertions and flows live in the test classes. (Selenium)
Key benefits when you apply POM correctly:
- Maintainability: Change a locator once in the page class, and all tests benefit.
- Reusability: The same page class can be used by many test cases and even multiple suites.
- Readability: Tests read like user flows instead of low-level WebDriver calls.
- Stability: Centralized, well-thought-through locators tend to be more robust across UI changes. (QA Touch)
Core Building Blocks of a Page Object
A Page Object is just a normal Java class following a simple structure:
- Private fields for locators (
ByorWebElement). - A constructor that receives a
WebDriver. - Public methods that model user actions and queries on the page.
Example Scenario: Login Page (No POM)
First, let's look at the "before" picture—tests that don't use POM:
// Bad example: raw WebDriver usage in test
@Test
public void login_with_valid_credentials() {
WebDriver driver = new ChromeDriver();
driver.get("https://example.com/login");
driver.findElement(By.id("email")).sendKeys("user@example.com");
driver.findElement(By.id("password")).sendKeys("Password123");
driver.findElement(By.id("login-btn")).click();
String welcomeText = driver.findElement(By.cssSelector("h1.welcome")).getText();
assertEquals(welcomeText, "Welcome back!");
driver.quit();
}
What's wrong here?
- Locators are scattered inside the test.
- If the login page changes, every test that uses it must be edited.
- There's no clear separation between how we interact with the page and what the test is asserting.
Example Scenario: Login Page (With Plain POM)
Now let's refactor using a Page Object.
LoginPage.java
public class LoginPage {
private WebDriver driver;
private By emailInput = By.id("email");
private By passwordInput = By.id("password");
private By loginButton = By.id("login-btn");
private By welcomeHeading = By.cssSelector("h1.welcome");
public LoginPage(WebDriver driver) {
this.driver = driver;
}
public void open() {
driver.get("https://example.com/login");
}
public void loginAs(String email, String password) {
driver.findElement(emailInput).clear();
driver.findElement(emailInput).sendKeys(email);
driver.findElement(passwordInput).clear();
driver.findElement(passwordInput).sendKeys(password);
driver.findElement(loginButton).click();
}
public String getWelcomeMessage() {
return driver.findElement(welcomeHeading).getText();
}
}
LoginTest.java
public class LoginTest {
private WebDriver driver;
private LoginPage loginPage;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
loginPage = new LoginPage(driver);
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void login_with_valid_credentials() {
loginPage.open();
loginPage.loginAs("user@example.com", "Password123");
assertEquals(loginPage.getWelcomeMessage(), "Welcome back!");
}
}
Why is this better?
- All locators for the login page live in one place.
- Tests read more like business flows:
open(),loginAs(...),getWelcomeMessage(). - You can easily reuse
LoginPagefor other tests (e.g., "invalid credentials", "locked account").
From a stability perspective, if you decide to swap id="login-btn" with a better, more semantic selector, you change it only in LoginPage.java, not in every test.
Implementing the Page Object Model in Selenium (Plain POM)
Let's generalize what we just did and highlight a few patterns.
Good Practices in Plain POM
- Keep WebDriver private: Only the page class should use it directly.
- Expose actions, not elements: Tests should call methods like
clickLoginButton()instead of reaching into the page to calldriver.findElement(...)again. - Assert in tests, not pages: Pages should expose information (
getWelcomeMessage()); tests should assert on that information.
Anti-pattern: Asserting inside the Page Object
// Bad: mixing assertions into the page object
public void verifyWelcomeMessage(String expectedText) {
String actual = driver.findElement(welcomeHeading).getText();
assertEquals(actual, expectedText); // Assertion here
}
This couples page objects with a specific test framework and makes reusing them harder. Instead:
// Good: provide data to the test
public String getWelcomeMessage() {
return driver.findElement(welcomeHeading).getText();
}
Now any test framework (JUnit, TestNG, custom runner) can use this page.
What Is Page Factory in Selenium?
While POM tells you how to structure your classes, Page Factory is a Selenium-provided helper for implementing POM using annotations like @FindBy and a static initElements method. (Selenium)
In Page Factory:
- You declare
WebElementfields with@FindBy. - Selenium populates them when you call
PageFactory.initElements(driver, this). - Optionally, you can use
AjaxElementLocatorFactoryfor lazy initialization and support for dynamic/Ajax-heavy UIs. (Stack Overflow)
Login Page with Page Factory
LoginPage.java
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
private WebDriver driver;
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "login-btn")
private WebElement loginButton;
@FindBy(css = "h1.welcome")
private WebElement welcomeHeading;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void open() {
driver.get("https://example.com/login");
}
public void loginAs(String email, String password) {
emailInput.clear();
emailInput.sendKeys(email);
passwordInput.clear();
passwordInput.sendKeys(password);
loginButton.click();
}
public String getWelcomeMessage() {
return welcomeHeading.getText();
}
}
LoginTest.java
public class LoginTest {
private WebDriver driver;
private LoginPage loginPage;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
loginPage = new LoginPage(driver);
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void login_with_valid_credentials() {
loginPage.open();
loginPage.loginAs("user@example.com", "Password123");
assertEquals(loginPage.getWelcomeMessage(), "Welcome back!");
}
}
Here we have the same behaviour as the plain POM, but:
- Locators are declared as
WebElementfields. - There's less boilerplate for
findElementcalls. - Tests are slightly more readable.
Using Lazy Initialization with AjaxElementLocatorFactory
If your application uses a lot of dynamically loaded elements (e.g., SPA frameworks, heavy Ajax), Page Factory's lazy loading can help by waiting for elements only when used.
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
public class LoginPage {
private WebDriver driver;
@FindBy(id = "email")
private WebElement emailInput;
// ... other @FindBy elements
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(
new AjaxElementLocatorFactory(driver, 10),
this
);
}
// methods...
}
The 10 here is a timeout in seconds. Each WebElement is only looked up when it's first used, which can help with elements that appear after Ajax calls or transitions. (Stack Overflow)
Be careful though: lazy initialization doesn't replace explicit waits. You still want to use WebDriverWait for complex conditions (e.g., "element is clickable", "Ajax request is completed").
Page Object Model in Selenium vs Page Factory: Key Differences
From a high level:
- Page Object Model is a design pattern: a way to structure your test code.
- Page Factory is a Selenium class that helps implement that pattern with annotations and automatic element initialization. (BrowserStack)
Here's a comparison of different approaches:
Plain POM (By locators)
- How Elements Are Defined:
Byfields anddriver.findElementcalls - Initialization: You call
findElementmanually - When It Shines: Teams new to POM, highly dynamic locators
- Trade-offs: More boilerplate, but very explicit and flexible
POM with Page Factory
- How Elements Are Defined:
@FindBy-annotatedWebElementfields - Initialization:
PageFactory.initElements(...) - When It Shines: Stable locators, lots of elements per page
- Trade-offs: Harder to handle truly dynamic locators
POM + Page Factory + AjaxElementLocatorFactory
- How Elements Are Defined:
@FindBywith lazy initialization - Initialization:
AjaxElementLocatorFactorywrapper - When It Shines: Ajax-heavy pages where elements load late
- Trade-offs: Must understand lazy loading; debugging can be trickier
No POM (raw WebDriver in tests)
- How Elements Are Defined: Locators inside test methods
- Initialization: None, all inline in tests
- When It Shines: Almost never a good choice in serious projects
- Trade-offs: Extremely hard to maintain and refactor
In practice, many teams start with plain POM, then adopt Page Factory where it adds clarity and removes boilerplate. Some frameworks, especially in Selenium 4+ ecosystems, stick with plain POM for maximum flexibility. (testleaf.com)
Real-World Workflow: From Raw Tests to Robust Page Objects
Let's take a small flow and see how POM + Page Factory changes it.
Scenario: User Sign-Up
Flow:
- Open sign-up page.
- Fill full name, email, password.
- Accept terms.
- Click "Create account".
- Verify confirmation message.
Bad pattern (all in one test):
@Test
public void user_can_sign_up() {
driver.get("https://example.com/signup");
driver.findElement(By.id("fullName")).sendKeys("Test User");
driver.findElement(By.id("email")).sendKeys("test.user@example.com");
driver.findElement(By.id("password")).sendKeys("Password123!");
driver.findElement(By.id("terms")).click();
driver.findElement(By.id("create-account")).click();
String message = driver.findElement(By.id("signup-message")).getText();
assertEquals(message, "Account created successfully");
}
If any locator changes, this test must be edited. If you have 20 sign-up tests, all 20 break.
Better pattern (Page Factory Page Object):
public class SignUpPage {
private WebDriver driver;
@FindBy(id = "fullName")
private WebElement fullNameInput;
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "terms")
private WebElement termsCheckbox;
@FindBy(id = "create-account")
private WebElement createAccountButton;
@FindBy(id = "signup-message")
private WebElement signupMessage;
public SignUpPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void open() {
driver.get("https://example.com/signup");
}
public void signUp(String fullName, String email, String password, boolean acceptTerms) {
fullNameInput.clear();
fullNameInput.sendKeys(fullName);
emailInput.clear();
emailInput.sendKeys(email);
passwordInput.clear();
passwordInput.sendKeys(password);
if (acceptTerms && !termsCheckbox.isSelected()) {
termsCheckbox.click();
}
createAccountButton.click();
}
public String getSignupMessage() {
return signupMessage.getText();
}
}
Test:
@Test
public void user_can_sign_up() {
SignUpPage signUpPage = new SignUpPage(driver);
signUpPage.open();
signUpPage.signUp("Test User", "test.user@example.com", "Password123!", true);
assertEquals(signUpPage.getSignupMessage(), "Account created successfully");
}
Now you can write multiple tests (invalid email, missing full name, etc.) reusing the same SignUpPage.
Best Practices Summary
Here's a quick checklist when applying the Page Object Model in Selenium and Page Factory:
-
Keep test logic and page logic separate
- Tests = flows and assertions.
- Page objects = locators and UI interactions.
-
Expose behaviours, not WebElements
- Methods like
loginAs(),fillSearchForm(),submitOrder()are easier to read and reuse than getters for elements.
- Methods like
-
Use meaningful, consistent naming
- Names like
clickLoginButton()andenterEmail()make tests self-documenting and help team collaboration.
- Names like
-
Centralize and review locators
- Put all locators for a page in a single class and periodically review them for stability and performance.
-
Avoid brittle locators
- Prefer IDs, stable CSS selectors, and semantic attributes over long, brittle XPath expressions unless absolutely necessary.
-
Don't mix assertions into pages
- Return values or states from page methods and assert in test classes for better reuse and cleaner responsibilities.
-
Leverage Page Factory sparingly and intentionally
- Use it when annotations and automatic initialization clearly reduce boilerplate; don't force it where dynamic locators are common.
-
Use lazy initialization wisely
- Combine
AjaxElementLocatorFactorywith explicit waits where required; don't rely on lazy loading as your only sync mechanism.
- Combine
-
Keep page objects small and focused
- If a page grows huge, split it into smaller "component" objects (header, sidebar, modal) to maintain readability.
-
Continuously refactor
- Just like production code, your automation code deserves regular refactoring as flows and UIs evolve.
FAQ
1. When should I use the Page Object Model in Selenium?
You should use POM as soon as you have more than a handful of UI tests. It quickly pays off in maintainability because locators and interactions are centralized, making it easier to evolve the UI without breaking all your tests. (ACCELQ)
2. What's the difference between POM and Page Factory?
POM is a design pattern that organizes your test code around page classes. Page Factory is a Selenium helper that uses annotations like @FindBy and the initElements method to create and initialize elements in those page classes. (BrowserStack)
3. Is Page Factory required to use the Page Object Model in Selenium?
No. You can use POM with plain By locators and findElement calls. Page Factory simply removes some boilerplate and adds features like lazy initialization, but the structural benefits of POM do not depend on it. (Selenium)
4. How can I make my Page Factory elements more stable?
Use robust locators (IDs, stable CSS selectors, data attributes) and consider combining Page Factory with explicit waits (WebDriverWait) for dynamic elements. Lazy initialization via AjaxElementLocatorFactory helps, but it's not a replacement for good locator strategy and synchronization. (Stack Overflow)
5. What are the most common mistakes beginners make with POM and Page Factory?
Common mistakes include:
- Putting assertions and business logic inside page objects.
- Overusing complex XPath locators instead of simpler CSS/ID-based ones.
- Creating "god" pages with hundreds of elements instead of smaller, component-based pages.
- Confusing POM (pattern) with Page Factory (implementation detail) and thinking they must always be used together. (QAwerk)
Conclusion
The Page Object Model in Selenium, with or without Page Factory, is one of the most powerful ways to keep your UI test automation clean, maintainable, and scalable. By encapsulating locators and interactions inside dedicated page classes, you dramatically reduce duplication and make your tests far easier to read and evolve over time.
Page Factory builds on top of this pattern with annotations and lazy initialization, which can simplify element management and improve performance on large projects—especially when used thoughtfully with good locator design and explicit waits.
If your existing Selenium tests are full of copy-pasted locators and fragile flows, start by extracting just one critical page into a Page Object. Once you experience the benefits, you'll likely want to refactor the rest of your suite around POM and adopt Page Factory wherever it makes your code clearer and more robust.
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