PLAYWRIGHT JAVA • ZERO → HERO • BLOG #4
Learn From What Is Already Built Well
A code-level study of real Java Playwright frameworks, design rationale, trade-offs and enterprise lessons

Playwright Java Framework Architecture: Learn From What Is Already Built Well

Blog #4 in the Playwright Java Zero → Hero series

The purpose of this article is simple: study Playwright Java frameworks, understand the design decisions behind them, identify their likely engineering rationale and impact, and turn those lessons into decisions you can apply to your own automation framework.


1. The five-color learning method

When looking at somebody else’s framework, there is a dangerous temptation to say:

“They created a BrowserFactory, so I should create a BrowserFactory too.”

That is exactly the wrong lesson.

Instead, every important design decision in this article is evaluated through five lenses:

Color Meaning Question
🔵 OBSERVED What the repository actually does What is in the code?
🟢 DOCUMENTED What the author explicitly explains What problem does the author say they are solving?
🟠 INFERRED Our engineering interpretation Why was this probably designed this way?
🔴 IMPACT Potential consequence What happens when the framework grows?
🟣 MY TAKE Recommendation What would I keep/change in a new enterprise project?

This distinction matters because we can inspect source code, but we cannot honestly claim to know an author’s private thought process unless they documented it.

The goal is therefore not to pretend certainty. The goal is to make the strongest engineering inference possible from the evidence.


2. The first framework: iamcharankumar/playwright_test_framework

The repository describes itself as a Java + Playwright end-to-end framework designed for scalability and maintainability. It uses Java 17, Maven and TestNG and includes multiple browsers, parallel execution, CDP-based monitoring, reporting integrations and GitHub Actions. At the time of this analysis, GitHub showed 38 stars and 13 forks.

That combination makes it a useful case study because it has already crossed several boundaries that a simple POM example never reaches.

Why I chose it as the primary example

The repository exposes several architectural concerns:

Tests
  ↓
Browser lifecycle
  ↓
BrowserFactory
  ↓
Browser / Context / Page
  ↓
Observability
  ↓
Reporting
  ↓
CI/CD

The README also documents a browser creation flow, parallel Maven commands, CDP monitoring, reporting, Codecov and GitHub Actions.

That gives us enough evidence to discuss not only what the framework contains, but why those layers are useful.


3. Start with the architecture, not the code

The repository’s documented browser flow is:

Client
  ↓
BrowserManager.getBrowserPage()
  ↓
Read browser / run-mode configuration
  ↓
BrowserFactory
  ↓
Browser implementation
  ↓
BrowserContext
  ↓
Page

The author explicitly documents this sequence and says that cleanup is performed through destroyBrowserPage().

🔵 OBSERVED

There is a deliberate separation between:

  • asking for a Page
  • choosing the browser
  • creating the browser session
  • creating the BrowserContext
  • returning the Page
  • cleaning up

🟢 DOCUMENTED

The README describes BrowserManager as the entry point for requesting a browser page and BrowserFactory as the mechanism that selects the appropriate browser implementation.

🟠 INFERRED — why would an engineer do this?

The likely motivation is policy centralization.

Imagine 150 tests doing this:

if (browser.equals("chrome")) {
    // launch Chromium
}
else if (browser.equals("firefox")) {
    // launch Firefox
}

That would make the tests responsible for infrastructure.

Instead:

TEST
 │
 │ "Give me a Page"
 ▼
BrowserManager
 │
 ├── Which browser?
 ├── Which run mode?
 ├── Which options?
 └── Which lifecycle?

The test becomes a consumer rather than an owner of browser infrastructure.

🟢 POSITIVE IMPACT

This gives the framework a single place to change:

  • browser selection
  • headless/local behavior
  • launch options
  • context creation
  • lifecycle cleanup

🔴 POTENTIAL IMPACT AT SCALE

There is a counter-risk.

If BrowserManager eventually becomes responsible for:

browser creation
context creation
page creation
screenshots
tracing
logging
retry
configuration
reporting
network monitoring
cleanup

then the abstraction becomes a God class.

The lesson is important:

Centralization is good. Centralizing unrelated responsibilities is not.

🟣 MY TAKE

I would keep the boundary, but define ownership explicitly:

BrowserProvider
    → owns browser process

ContextProvider
    → owns test session

PageProvider
    → owns page creation

TestLifecycle
    → owns setup/teardown

Observability
    → owns trace/screenshots/network evidence

You don’t necessarily need five classes on day one. The important thing is knowing what each responsibility means.


4. BrowserFactory: abstraction or unnecessary ceremony?

A factory is one of those patterns that can be either excellent or completely unnecessary.

The repository explicitly uses a BrowserFactory between the manager and the concrete browser implementation.

Conceptually:

                 BrowserManager
                       │
                       ▼
                 BrowserFactory
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
       Chromium      Firefox       Edge

🔵 OBSERVED

Browser selection is not embedded directly inside the test classes.

🟠 INFERRED

The likely thought process is:

“Browser-specific construction is infrastructure. I don’t want my test layer to know how it happens.”

That is sound.

But here is the architect question

Does Playwright itself already provide:

playwright.chromium()
playwright.firefox()
playwright.webkit()

Yes.

So why add a factory?

Because the framework may not merely be selecting an API object. It may be deciding:

Browser
+ launch mode
+ channel
+ arguments
+ downloads
+ proxy
+ tracing
+ environment
+ application-specific defaults

Once those decisions become meaningful, the factory becomes more defensible.

🔴 Potential downside

If the factory is only:

if chrome → playwright.chromium()
if firefox → playwright.firefox()

then it may be abstraction for abstraction’s sake.

🟣 MY TAKE

Use a factory when it hides policy, not merely syntax.

This is a useful framework-design rule far beyond Playwright.


5. BrowserContext: probably the most important design decision

Playwright’s BrowserContext is an isolated browser session. Playwright recommends using isolated contexts to prevent tests from interfering with one another.

The case-study framework creates a BrowserContext before creating the Page.

That is a very good Playwright-native design.

Think:

Browser
 │
 ├── Context A
 │     └── Page A
 │
 ├── Context B
 │     └── Page B
 │
 └── Context C
       └── Page C

Why not one Page for everything?

Because a Page is not your isolation boundary.

Cookies, storage and session state belong to the browser context.

This is one of the biggest conceptual differences people coming from Selenium often miss.

🟣 MY TAKE

In an enterprise Playwright framework, I would make this ownership rule explicit:

One test owns one BrowserContext. The test may create one or more Pages inside that context.

That gives you a clean mental model for parallelism, authentication and cleanup.


6. Parallel execution: ThreadLocal is solving only one part of the problem

The repository documents parallel execution through Maven/TestNG properties, including thread counts and DataProvider thread counts. It also documents commands that execute groups in parallel across multiple browsers and modes.

This is where framework architecture gets interesting.

A parallel run looks like:

                    TEST RUN
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
       Worker 1     Worker 2     Worker 3
          │            │            │
       Context A    Context B    Context C
          │            │            │
        Page A       Page B       Page C

The framework must answer:

“When Worker 2 asks for Page, how do we make sure it receives Worker 2’s Page?”

One common solution is ThreadLocal<Page>.

🔵 OBSERVED

JapneetSachdeva1’s framework explicitly uses a ThreadLocal<Page> inside PlaywrightFactory. The factory creates a browser, creates a context, creates a Page and stores that Page in the thread-local reference.

🟠 INFERRED

The design intention is straightforward:

Thread 1 → Page 1
Thread 2 → Page 2
Thread 3 → Page 3

so code can retrieve the current Page without passing it through every method.

🟢 Why this is useful

Without thread-local access, you might end up with:

test(page);

pageObject(page);

component(page);

utility(page);

everywhere.

Thread-local access can reduce plumbing.

🔴 But don’t confuse this with test isolation

ThreadLocal<Page> does not guarantee:

  • isolated test data
  • isolated database records
  • isolated files
  • isolated environment state
  • correct BrowserContext lifecycle
  • safe static configuration

It solves thread-local lookup.

That’s it.

🟣 MY TAKE

If using ThreadLocal, make lifecycle explicit:

@BeforeMethod
   ↓
Create Context
   ↓
Create Page
   ↓
Store Page
   ↓
Test
   ↓
Close Page/Context
   ↓
ThreadLocal.remove()

The last step is frequently forgotten.

The stronger enterprise design is to make Context the lifecycle owner, with Page as a child resource.


7. A second framework gives us an important contrast

Now look at JapneetSachdeva1/PlaywrightJavaFramework - https://github.com/JapneetSachdeva1/PlaywrightJavaFramework.

Its README explicitly lists:

  • Page Object Model
  • Maven
  • Extent reporting
  • TestNG
  • JSON data-driven execution
  • TestNG data-driven execution.

The repository structure separates:

src/main/java
 ├── constants
 ├── factory
 ├── pages
 └── utils

and the test side contains:

src/test/java
 ├── base
 └── pageTest

The GitHub tree confirms these package boundaries.

Why is this interesting?

It demonstrates that two valid frameworks can solve the same problem with different levels of abstraction.

The first framework emphasizes:

BrowserManager
BrowserFactory
Browser implementations
CDP
parallel execution
CI/CD

The second is more compact:

PlaywrightFactory
Pages
Utils
Base
Tests

Neither structure should automatically be copied.

The right question is:

What complexity does my application actually require?


8. PlaywrightFactory: a compact alternative

Japneet’s PlaywrightFactory is particularly useful for learning because it is small enough to understand quickly.

The source shows:

  • a Playwright instance
  • a Browser
  • a BrowserContext
  • ThreadLocal<Page>
  • browser selection
  • context creation
  • page creation
  • screenshot capture.

The key lifecycle is essentially:

Playwright
    ↓
Browser
    ↓
BrowserContext
    ↓
Page

🟢 What I like

It is easy for a new engineer to understand.

That matters.

A framework that nobody understands becomes a maintenance problem.

🔴 What I would question

The class combines several concerns:

browser creation
thread-local page storage
navigation
screenshot generation

That is a sign that the class may eventually grow too much.

🟣 My evolution

I’d split it when the framework grows:

PlaywrightManager
BrowserProvider
ContextProvider
PageStore
ArtifactManager

But I would not necessarily split all of them on day one.

This leads to another important principle:

Don’t design a 5,000-test framework when you currently have 20 tests. Design so that the next architectural boundary is easy to introduce.


9. Page Object Model: what the second framework gets right

The repository has dedicated page classes such as:

LoginPage
HomePage
AccountPage
RegisterPage

and explicitly identifies POM as one of its framework features.

This gives us the classic:

Test
 ↓
Page Object
 ↓
Locator
 ↓
Playwright

The important question isn’t “Should I use POM?”

The better question is:

What should a Page Object expose?

Bad:

loginPage.username.fill(...);
loginPage.password.fill(...);
loginPage.loginButton.click();

This leaks UI mechanics into tests.

Better:

loginPage.login(username, password);

The test expresses business intent.

🟣 MY TAKE

For enterprise Playwright:

Page
 ├── HeaderComponent
 ├── NavigationComponent
 ├── TableComponent
 └── FormComponent

is often better than creating enormous Page Objects.

Playwright’s Locator model supports composition and locator chaining, which fits this component approach well.


10. Why the component model matters

Imagine an enterprise application with:

IncidentPage
ProblemPage
ChangePage
KnowledgePage

and all four contain:

Header
Navigation
Search
Data table
Pagination
Notifications
User menu

Copying these locators into every Page Object creates maintenance debt.

Instead:

                    Base application
                          │
              ┌───────────┴───────────┐
              ▼                       ▼
       HeaderComponent          NavigationComponent
              │
              ▼
       reusable Locators

Then:

public class IncidentPage {

    private final HeaderComponent header;
    private final NavigationComponent navigation;

}

That is composition, not inheritance.


11. CDP monitoring: a particularly clever idea

The primary repository documents a CDP monitoring implementation:

Test
 ↓
CdpUtils
 ↓
CDPSessionHandlerImpl
 ↓
Playwright CDPSession
 ↓
Browser network event
 ↓
Framework

The documented use case is monitoring HTTP/network failures such as 404/500 responses.

🟢 Why this is valuable

A UI test can fail with:

Element not found

but the real cause could be:

GET /api/incidents → 500

The framework can capture that additional signal.

This is a classic observability principle:

Don’t only record what the test expected. Record evidence about what the system actually did.

🔴 But there is an important architectural concern

CDP is Chromium-oriented.

If your framework claims to support:

Chromium
Firefox
WebKit

you need to distinguish:

Playwright cross-browser capability

from:

CDP-specific diagnostics

They are not the same thing.

🟣 My recommendation

Define an abstraction:

interface NetworkMonitor {
    void start(Page page);
    void stop();
}

Then:

Chromium
   ↓
CDP NetworkMonitor

Firefox / WebKit
   ↓
Playwright event-based monitor
or
No-op / alternate implementation

Now the framework doesn’t accidentally make Chromium-specific instrumentation a hidden dependency of every test.


12. Maven as a runtime control plane

The primary repository has a useful pattern: browser, run mode, groups and concurrency are selected through Maven properties, and the README provides commands for different combinations.

For example, the documented commands vary:

mvn clean test -Dbrowser=firefox ...

or:

mvn clean test -Drunmode=headless ...

or combinations including thread counts.

🟢 Why this is good

CI can change behavior without modifying Java source.

Same code
   │
   ├── local
   ├── headless
   ├── Chrome
   ├── Firefox
   └── Edge

🔴 What I would improve

Raw strings create weak contracts:

browser=firefox
browser=FireFox
browser=fire-fox
browser=abc

The framework should validate configuration at startup.

For example:

enum BrowserType {
    CHROMIUM,
    FIREFOX,
    WEBKIT,
    CHROME
}

Then convert the external string into a validated internal representation.

Architect principle

Configuration should fail fast at the boundary.

A typo should fail before the first test starts—not after a browser factory returns null.


13. Reporting is not the same as observability

The primary framework integrates screenshots, ReportPortal, Discord notifications and Codecov/GitHub Actions.

This is useful, but let’s separate the concepts.

Reporting

Answers:

Did the test pass?

Observability

Answers:

Why did it fail?

For a production-grade automation platform, useful evidence might include:

Test result
    +
Screenshot
    +
Trace
    +
Console
    +
Network evidence
    +
Environment
    +
Browser
    +
Test data identifier

That is much more powerful than a screenshot alone.


14. One of the best lessons: don’t copy Selenium architecture blindly

This is particularly important for someone coming from Selenium.

Suppose your old Selenium framework has:

DriverFactory
DriverManager
WaitManager
ElementUtils
BrowserUtils
ScreenshotUtils
WindowUtils
FrameUtils

You might be tempted to reproduce:

PlaywrightFactory
PlaywrightManager
WaitManager
LocatorUtils
BrowserUtils
...

Don’t.

Playwright already provides strong primitives for:

  • Locator
  • auto-waiting
  • assertions
  • BrowserContext
  • tracing
  • downloads
  • pages
  • frames
  • API requests.

For example, Playwright documents Locators as the central abstraction for auto-waiting and retryability, and recommends user-facing locator strategies such as role, label, text, placeholder and test ID.

So your framework should amplify Playwright’s model, not rebuild Selenium’s model around it.


15. What I would keep from these frameworks

After studying these approaches, my “keep” list is:

✅ Keep

Central browser policy

Because tests should not know infrastructure details.

BrowserContext isolation

Because isolation is fundamental to reliable parallel execution.

Page Objects / Components

Because tests should express business intent.

Runtime configuration

Because CI needs to control execution without source changes.

Parallel execution

Because scale requires concurrency.

Failure artifacts

Because debugging is part of automation engineering.

Network diagnostics

When they provide meaningful root-cause evidence.

CI integration

Because a framework that only works from an engineer’s laptop isn’t an automation platform.


16. What I would change

1. Make ownership explicit

Test
 ↓ owns
BrowserContext
 ↓ owns
Page(s)

Avoid ambiguous lifecycle ownership.

2. Keep ThreadLocal at the edge

Don’t let ThreadLocal become a global dependency throughout the framework.

3. Separate infrastructure from observability

Browser creation shouldn’t also own screenshots, reports and network monitoring.

4. Validate configuration

Convert strings into validated configuration objects early.

5. Prefer Playwright-native capabilities

Don’t create utilities for functionality Playwright already handles well.

6. Design for components

Avoid giant Page Objects.

7. Make diagnostics cross-browser aware

Especially if using CDP.

8. Keep the framework understandable

Complexity itself is a cost.


17. My reference architecture after learning from these projects

I’d start with:

src/
│
├── main/java/
│   └── framework/
│       ├── config/
│       │   └── FrameworkConfig
│       │
│       ├── browser/
│       │   ├── BrowserProvider
│       │   ├── ContextProvider
│       │   └── PageStore
│       │
│       ├── pages/
│       │   ├── LoginPage
│       │   └── DashboardPage
│       │
│       ├── components/
│       │   ├── Header
│       │   └── DataTable
│       │
│       ├── api/
│       │   └── ApiClient
│       │
│       ├── observability/
│       │   ├── TraceManager
│       │   ├── ScreenshotManager
│       │   └── NetworkMonitor
│       │
│       └── testdata/
│
└── test/java/
    ├── tests/
    └── fixtures/

Notice what is not here:

WaitManager
ElementUtils
ClickUtils
BrowserUtils
JavaScriptUtils

Those are often signs that the framework is compensating for weaknesses that Playwright already addresses.


18. The most important lesson

After looking at real frameworks, the biggest lesson is not:

“Use BrowserFactory.”

It is:

Understand why an abstraction exists before adopting it.

Ask five questions:

1. What problem does this solve?
             ↓
2. Why does the test layer need this abstraction?
             ↓
3. What complexity does it remove?
             ↓
4. What new complexity does it introduce?
             ↓
5. Will that trade-off still make sense at 5,000 tests?

That is framework architecture.


19. A practical exercise for your own project

Since you’re building a Java Playwright framework, don’t just read this article.

Take your current project and create these five boxes:

Browser lifecycle
Test lifecycle
Page lifecycle
Configuration
Observability

For each one, write:

Who creates it?
Who owns it?
Who closes it?
Who can access it?
Can it be used safely in parallel?

If you cannot answer those five questions, your framework architecture isn’t finished yet.


20. Interview questions from this case study

Q1. Why use BrowserFactory?

Good answer:

To centralize browser-specific construction and configuration so test code depends on a stable abstraction rather than browser-specific launch logic. I would avoid the factory if it only forwards directly to Playwright without adding meaningful policy.

Q2. Why BrowserContext?

It is Playwright’s isolation boundary. It provides independent browser sessions and separates cookies, storage and authentication state.

Q3. Why ThreadLocal?

It provides thread-local access to the Page when tests execute in parallel, reducing the need to pass Page through every layer. But ThreadLocal alone does not guarantee test-data or resource isolation.

Q4. Is ThreadLocal mandatory?

No. It is one implementation strategy. A context/page fixture or dependency-injection model can be cleaner depending on the TestNG architecture.

Q5. Why use CDP?

For lower-level browser/network diagnostics that can provide information beyond the UI failure itself. But I would isolate CDP behind an abstraction because cross-browser behavior matters.

Q6. Should every Playwright framework have a BrowserFactory?

No. It depends on whether browser construction contains meaningful policy.

Q7. Should we recreate Selenium’s WaitManager in Playwright?

Usually no. Playwright’s Locator/actionability and web-first assertions already provide synchronization behavior. Add targeted waits only where the application scenario requires a specific state or event.

Q8. What’s the difference between a framework and a collection of utilities?

A framework defines lifecycle, ownership, contracts and extension points. Utilities merely provide helper functions.


21. The final mental model

Remember this:

                 GOOD FRAMEWORK
                       │
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼
     OWNERSHIP      CONTRACTS      ISOLATION
        │              │              │
        └──────────────┼──────────────┘
                       ▼
                  OBSERVABILITY
                       │
                       ▼
                    SCALE

The repositories we studied are valuable not because every line should be copied, but because they demonstrate real solutions to real framework problems.

The best thing you can take from them is not their class names.

It is their engineering reasoning.


Sources and repositories

Primary case study
iamcharankumar/playwright_test_framework

Secondary case study
JapneetSachdeva1/PlaywrightJavaFramework

Additional repository for future comparison
bhargavkumar-65/PlaywrightJava

Official Playwright Java documentation
Locators
BrowserContext API
Locator API
Writing tests

Source-note: Repository claims and code observations in this article are based on the public repositories and their documentation. Statements labelled “INFERRED” or “MY TAKE” are engineering interpretation/recommendation, not claims about the authors’ private intent.

Comments

Popular posts from this blog

Simple Data Driven Framework script

Reading and writting data from .xlsx spreadsheet using Apache POI API

Generalized Apache POI script for reading and writing to .xlsx files