---
title: Page Factory vs Plain By Locators | TestingBot
description: Search for &quot;page object model selenium&quot; and a large share of
  tutorials use `PageFactory`, `@FindBy` and `initElements`. The page objects in this…
source_url:
  html: https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators
  md: https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators.md
---

Course

[Page Object Model and Framework Design](https://testingbot.com/resources/courses/page-object-model)

Chapter 5 of 10

1. [1 What Is the Page Object Model?](https://testingbot.com/resources/courses/page-object-model)
2. [2 Your First Page Object in Selenium](https://testingbot.com/resources/courses/page-object-model/first-page-object)
3. [3 Page Object Model in Playwright](https://testingbot.com/resources/courses/page-object-model/playwright-page-objects)
4. [4 Components and Fluent Page Objects](https://testingbot.com/resources/courses/page-object-model/components-and-fluent-pages)
5. [5 Page Factory vs Plain By Locators](https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators)
6. [6 Test Data, Config and Environments](https://testingbot.com/resources/courses/page-object-model/test-data-and-config)
7. [7 Test Automation Framework Structure](https://testingbot.com/resources/courses/page-object-model/framework-layout)
8. [8 Run the Framework in Parallel on a Grid](https://testingbot.com/resources/courses/page-object-model/parallel-on-a-grid)
9. [9 POM Anti-Patterns and When to Skip POM](https://testingbot.com/resources/courses/page-object-model/anti-patterns)
10. [10 Knowledge check](https://testingbot.com/resources/courses/page-object-model/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 5 of 10 Intermediate 7 min read 2 quick checks Updated Nov 2026 

# Page Factory vs Plain By Locators
On this page
1. [What Page Factory is](https://testingbot.com#what-page-factory-is)
2. [Why the Selenium docs moved on](https://testingbot.com#why-the-selenium-docs-moved-on)
3. [The plain By version, again](https://testingbot.com#the-plain-by-version-again)
4. [Side by side](https://testingbot.com#side-by-side)
5. [What about Playwright?](https://testingbot.com#what-about-playwright)
6. [Migrating an existing Page Factory suite](https://testingbot.com#migrating-an-existing-page-factory-suite)

Search for "page object model selenium" and a large share of tutorials use `PageFactory`, `@FindBy` and `initElements`. The page objects in this course do not. This chapter shows what Page Factory does, rewrites our login page with it so you can compare, and explains why we, and the current Selenium documentation, default to plain `By` locators.

## What Page Factory is[#](https://testingbot.com#what-page-factory-is)

`PageFactory` is a Java support class in `selenium-support` (part of `selenium-java`). You annotate `WebElement` fields with `@FindBy`, call `PageFactory.initElements`, and every field is replaced by a **proxy**. The proxy finds the real element only when you call a method on it, and by default finds it again on every call.

Here is the login page from chapter 01, rewritten with Page Factory. It compiles and passes against Selenium 4.52.0:

```java
package com.example.pages;

import java.time.Duration;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class FactoryLoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

    @FindBy(id = "username")
    private WebElement username;

    @FindBy(id = "password")
    private WebElement password;

    @FindBy(id = "submit")
    private WebElement submit;

    @FindBy(id = "error")
    private WebElement error;

    public FactoryLoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(new AjaxElementLocatorFactory(driver, 10), this);
    }

    public FactoryLoginPage open() {
        driver.get("https://practicetestautomation.com/practice-test-login/");
        return this;
    }

    public FactoryLoginPage loginExpectingError(String user, String pass) {
        username.sendKeys(user);
        password.sendKeys(pass);
        submit.click();
        return this;
    }

    public String errorMessage() {
        return wait.until(ExpectedConditions.visibilityOf(error)).getText();
    }
}
```

It reads nicely: the fields look like the elements themselves, and the methods have no `driver.findElement` noise. That readability is why the pattern spread.

Three variations matter:

- `PageFactory.initElements(driver, this)` uses the default locator, which tries once and throws `NoSuchElementException` if the element is not there yet (subject only to any implicit wait).
- `new AjaxElementLocatorFactory(driver, 10)`, as above, retries the lookup for up to 10 seconds. That is the closest Page Factory gets to waiting.
- `@CacheLookup` on a field makes the proxy remember the element after the first lookup. Faster, but if the DOM node is replaced (any re-render, any navigation back) you get `StaleElementReferenceException`.

A `@FindBy` field also has `@CacheLookup`. After the form re-renders, the next `sendKeys` on it fails. With what?

 `NoSuchElementException` `TimeoutException` naming the locator `StaleElementReferenceException`

`@CacheLookup` makes the proxy remember the first element it found. Once that DOM node is replaced, the cached reference is stale.

## Why the Selenium docs moved on[#](https://testingbot.com#why-the-selenium-docs-moved-on)

The Selenium project's page about Page Factory now sits in the **Legacy** section of selenium.dev under "Selenium 2", with a banner saying it "describes Selenium 2 and 3 and is kept for reference". The current [Page Object Models page](https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/) uses plain `By` locators and does not mention Page Factory at all.

Page Factory is **not deprecated** : the class and annotations ship in Selenium 4.52.0 without `@Deprecated`, and existing suites keep working. But the documentation has clearly stopped recommending it, and the reasons are practical:

**1. It encourages the wrong kind of waiting.** A proxy field looks like a ready element, so the natural code is `submit.click()`. Whether that waits depends on which locator factory someone chose in the constructor, which the method body does not show. Even with `AjaxElementLocatorFactory`, the retry is "until the element exists", not "until it is visible" or "clickable". For our `#error` box, which exists from page load with opacity 0, "exists" is useless: you still need the explicit `visibilityOf` wait in `errorMessage()`. Once you write explicit waits anyway, the proxy adds nothing.

**2. It works on elements, waits work on locators.** `ExpectedConditions` has two families: `visibilityOfElementLocated(By)` re-finds the element on each poll, `visibilityOf(WebElement)` checks a specific element. With Page Factory you only have `WebElement`s, so you lose the `By`-based conditions, including `presenceOfAllElementsLocatedBy`, `numberOfElementsToBe`, and `invisibilityOfElementLocated`.

**3. It hides errors.** A misspelled `@FindBy` fails at first use, with a stack trace that goes through a dynamic proxy, and it fails the same way whether the element was missing, slow, or in a different frame. With `By` fields and a `visible(By)` helper, the `TimeoutException` names the locator and the condition.

**4. It does not exist outside Java.** The Python, JavaScript and Ruby Selenium bindings have no Page Factory. If your organisation writes tests in more than one language, plain locators keep the page objects recognisably the same pattern, as chapter 01 showed.

**5. Components get awkward.** Scoping a component's fields to a root element requires passing a `DefaultElementLocatorFactory` built around a `SearchContext`. It works, but most teams discover it only after their header component accidentally matched an element in the footer.

## The plain By version, again[#](https://testingbot.com#the-plain-by-version-again)

For comparison, the core of the chapter 01 page object:

```java
private final By submit = By.id("submit");
private final By error = By.id("error");

public String errorMessage() {
    return visible(error).getText();
}

private void submitCredentials(String user, String pass) {
    type(username, user);
    type(password, pass);
    click(submit);
}
```

Every interaction states its wait (`visible`, `click` via `elementToBeClickable`) through two small helpers in `BasePage`. Every lookup happens at the moment of use, so there is nothing to go stale. Locators are values you can pass around, log, or combine (`root.findElement(locator)` for components).

## Side by side[#](https://testingbot.com#side-by-side)

| | `@FindBy` + `PageFactory` | `By` fields + helpers |
| --- | --- | --- |
| Lookup timing | Lazy, on each call (unless `@CacheLookup`) | At the moment of use |
| Waiting | Only via locator factory, presence only | Explicit, any `ExpectedConditions` |
| Stale elements | Possible with `@CacheLookup` | Not possible (fresh lookup) |
| Error messages | Through a proxy | Name the locator and condition |
| Languages | Java only | Every binding |
| Lists | `List<WebElement>` with `@FindBy` | `findElements(By)` or wait conditions |
| Status | Legacy docs, not deprecated | Current Selenium docs |

## What about Playwright?[#](https://testingbot.com#what-about-playwright)

Playwright solves the problem Page Factory was trying to solve. A Playwright `Locator` is lazy like a proxy field, re-resolves on every action, and waits for actionability (visible, stable, enabled, receiving events) before each action. That is why the TypeScript page objects in chapter 02 can be plain classes with `readonly` locator fields and no helper methods.

The trade-off is the one we saw in chapter 02: Playwright's idea of "visible" includes `opacity: 0`, so the page object still has to encode what "showing" means for an element like `#error`.

## Migrating an existing Page Factory suite[#](https://testingbot.com#migrating-an-existing-page-factory-suite)

You do not need a big-bang rewrite. A safe order:

1. Add `BasePage` with `visible(By)`, `type(By, String)` and `click(By)` helpers.
2. Pick the page objects with the most flaky failures first (your CI history or test reports will show them).
3. For each, replace `@FindBy(id = "x") WebElement x;` with `By x = By.id("x");`, then fix every compile error by routing the use through a helper. The compiler finds every call site for you.
4. Remove `PageFactory.initElements` from the constructor last. A page object without any `@FindBy` fields gets nothing from it.

Run the suite after each page, and delete any `Thread.sleep` you find on the way: an explicit wait on the right condition replaces it.

Our [Selenium with Java and TestNG course](https://testingbot.com/resources/courses/selenium-testng) covers explicit waits in more depth, and the [TestNG documentation for TestingBot](https://testingbot.com/support/web-automate/selenium/java/testng) shows the same page objects against remote browsers.

You are migrating a Page Factory suite one page at a time. When do you remove `PageFactory.initElements` from a page object's constructor?

 First, so the compiler shows every call site Last, once that page object has no `@FindBy` fields left Never; it is needed for the `By` fields to work Only after every page in the suite is migrated

Replacing each `@FindBy` field with a `By` is what makes the compiler find the call sites. A page object without `@FindBy` fields gets nothing from `initElements`, so it goes last.

[Previous · Chapter 4 Components and Fluent Page Objects](https://testingbot.com/resources/courses/page-object-model/components-and-fluent-pages) [Next · Chapter 6 Test Data, Config and Environments](https://testingbot.com/resources/courses/page-object-model/test-data-and-config)

On this page

1. [What Page Factory is](https://testingbot.com#what-page-factory-is)
2. [Why the Selenium docs moved on](https://testingbot.com#why-the-selenium-docs-moved-on)
3. [The plain By version, again](https://testingbot.com#the-plain-by-version-again)
4. [Side by side](https://testingbot.com#side-by-side)
5. [What about Playwright?](https://testingbot.com#what-about-playwright)
6. [Migrating an existing Page Factory suite](https://testingbot.com#migrating-an-existing-page-factory-suite)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/page-object-model/certification)
