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
Playwrightinstance - 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
Post a Comment