skip to content

In Selenium's PageFactory, what does adding @CacheLookup to a proxied field change inside the element locator?

level: middleimportance: should knowfreq 57%

answer

  1. Ask what stops repeating between calls
  2. A marker annotation with no members
  3. The locator changes, not the proxy
  4. A boolean guards a stored element field
  5. First successful lookup wins from then on

basics

~20 s

It makes the locator keep the first element it finds and return that same instance from then on. Selenium stops sending a find command for that field, so every later call reuses one DOM node.

solid answer

~40 s

`@CacheLookup` is a marker annotation on a field, read once through `Annotations.isLookupCached()`. `DefaultElementLocator` copies that flag into its `shouldCache` field, and its `findElement()` then returns a stored `cachedElement` whenever one exists instead of calling `searchContext.findElement(by)` again. A `List<WebElement>` field behaves the same way through `cachedElementList` in `findElements()`. Nothing about the proxy changes — the field is still a `java.lang.reflect.Proxy`, and `LocatingElementHandler` still calls the locator on every invocation; it is the locator that stops going to the browser. On a taxi-dispatch board that means a `@CacheLookup` header field costs exactly one find command for the life of that page-object instance. The annotation's own contract is that the element never changes, so it fits only nodes the application never replaces.

go deeper

for a junior

Know that @CacheLookup is an opt-in annotation on a page-object field and that it means the element is looked up once instead of on every call.

for a middle

Explain the plumbing: Annotations.isLookupCached feeds DefaultElementLocator.shouldCache, which guards a stored element and a stored list inside the locator itself.

for a senior

Demonstrate that you know what the annotation promises on your behalf. An interviewer expects you to say which nodes on a page genuinely never change and why that precondition is yours to keep.

for a principal

Own the policy angle: a per-field annotation scattered by hand is hard to audit, and a shouldCache override in a custom locator makes the caching rule explicit in one place instead.

## A marker annotation with no members **`@CacheLookup`** lives in `org.openqa.selenium.support` and is declared `@Retention(RetentionPolicy.RUNTIME)` and `@Target(ElementType.FIELD)`. It carries **no attributes at all** — no timeout, no size, no scope. Its javadoc states the contract plainly: it marks a `WebElement` that *never changes*, meaning the same DOM instance will always be used. On a taxi-dispatch board, that fits a static toolbar or a page heading; it does not fit a ride row. ## Where the flag is read The annotation never reaches the proxy. The chain is short and worth being able to recite: 1. `DefaultElementLocator` is constructed with the `Field`, from which it builds an `Annotations` object. 2. `Annotations.isLookupCached()` returns whether `field.getAnnotation(CacheLookup.class)` is non-null. 3. The locator copies that boolean once, at construction, into its private `shouldCache` field, exposed through a `protected boolean shouldCache()` method that subclasses may override. So the decision is made **once per field, per page-object instance**, at the moment `PageFactory.initElements` decorates that field — not per call and not per test. ## What `findElement()` does differently Without the annotation, `DefaultElementLocator.findElement()` is a single line of work: run `searchContext.findElement(by)` and return the result. With it, the method gains a cache check and a cache fill: ```java public WebElement findElement() { if (cachedElement != null && shouldCache()) { return cachedElement; } WebElement element = searchContext.findElement(by); if (shouldCache()) { cachedElement = element; } return element; } ``` Read the ordering carefully: - The **first** call still goes to the browser. `@CacheLookup` does not make the lookup eager; nothing is resolved while `initElements` runs. - Only after a successful find is `cachedElement` populated, so a lookup that throws leaves the cache empty and the next call retries. - Every subsequent call short-circuits before `searchContext.findElement(by)` and issues **no WebDriver command at all**. ## Lists cache the same way `findElements()` mirrors it against a separate `cachedElementList` field. A `@CacheLookup` `List<WebElement>` of waiting-ride rows is queried once; from then on `size()`, `get(0)` and iteration all run against the list captured on that first call, however many rides the dispatcher has since assigned. The two caches are independent fields on the same locator, but a given page-object field only ever uses one of them, because a field is either a `WebElement` or a `List<WebElement>` and the proxy installed over it calls only the matching locator method. ## Default versus cached, side by side | behaviour | no `@CacheLookup` | with `@CacheLookup` | |---|---|---| | lookups during `initElements` | none | none | | find commands for *n* method calls | *n* | 1 | | element instance returned | freshly located each time | the first one, forever | | cache scope | not applicable | the locator, so one page-object instance | | list field | re-queried on every call | captured on the first call | | after a failed lookup | retried on the next call | retried, nothing was cached | ## What `@CacheLookup` does **not** change - The field still holds a `java.lang.reflect.Proxy`; the annotation never removes the indirection. - `LocatingElementHandler` still calls `locator.findElement()` on **every** method invocation — it has no idea caching exists. The saving happens one layer down. - The `By` is unchanged. Caching stores a result, not a different locator strategy. - Nothing is shared between page-object instances. Build a second `DispatchBoardPage` and you get fresh locators with empty caches. - Subclasses can take the decision away from the annotation entirely by overriding `shouldCache()`; `AjaxElementLocator` inherits the same caching machinery from its parent. ## Reading the trade honestly The annotation buys exactly one thing — **fewer round trips** — and it buys it by making a promise the test author, not Selenium, has to keep: that the node behind that `By` is never re-created. When that promise holds, a header or a static control on a dispatch board is a legitimate candidate, and the saving is real on a page object whose methods touch the same field repeatedly. When it does not hold, the locator has no way to notice; it will hand back the reference it stored and let the browser reject it. That is why experienced teams treat `@CacheLookup` as a narrow optimisation applied deliberately to individual fields, rather than as a default sprinkled over a page object — the annotation's own documentation states the "never changes" precondition rather than implying it.

  • Does @CacheLookup make the first lookup happen earlier?
    No. The field is still a proxy after `initElements`, and `DefaultElementLocator.findElement()` populates `cachedElement` only after a successful `searchContext.findElement(by)`. The first browser round trip still happens on the first method call through the field; the annotation changes what happens on the second call onwards.
  • What is the lifetime of the cached element?
    The locator's, which is the page-object instance's. `createLocator(Field)` is called once per field during `initElements`, so building a second `DispatchBoardPage` produces fresh locators with empty caches. There is no cache shared across page objects, across tests or across driver sessions.
  • Can code override the caching decision without touching the annotation?
    Yes. `DefaultElementLocator.shouldCache()` is `protected`, so a subclass can return something other than the annotation's value — for example caching every field, or none. A custom `ElementLocatorFactory` returning those locators applies the policy across a page object without editing any field annotation.

saying these in an interview costs you the question

  • Says @CacheLookup removes the proxy and stores a plain WebElement
  • Thinks caching happens per test rather than per page-object instance
  • Believes @CacheLookup makes the lookup eager at initElements time
  • Thinks the FieldDecorator reads @CacheLookup rather than the locator
  • Assumes a cached List field is re-queried on every size call