Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 24
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 5
CI/CD 6
  • GitHub Actions
  • Slack Reporting
QA Strategy 4
Case Studies 5
Blog/Automation
AutomationHalmurat T./October 12, 2021/4 min

Tight vs Loose Coupling in Java Test Design

Filed underjava/loose-coupling/tight-coupling
Tight vs Loose Coupling in Java Test Design

Table of Contents
  • What coupling actually means
  • Tight coupling example
  • Loose coupling example
  • Constructor injection is usually enough
  • When tight coupling is acceptable
  • Why this matters in automation code
  • A simple rule I use
  • Where teams overdo it
  • Bottom line

On this page

  • What coupling actually means
  • Tight coupling example
  • Loose coupling example
  • Constructor injection is usually enough
  • When tight coupling is acceptable
  • Why this matters in automation code
  • A simple rule I use
  • Where teams overdo it
  • Bottom line

Most coupling explanations stop at car-and-engine metaphors. In real Java code, coupling decides whether a change takes ten minutes or sends you through half the application just to update one behavior.

It also decides how easy your code is to test. That is the part I care about most.

This comes up constantly in automation code. The same design mistakes that make business logic hard to change are the ones that make page objects brittle, reporting code global, and parallel execution unsafe. If you’ve seen how shared state wrecks parallel test execution, you’ve already seen coupling debt in one of its most expensive forms.

What coupling actually means

Coupling is the amount of direct dependency one class has on another concrete implementation.

  • Tight coupling means a class knows too much about a concrete dependency and often creates it itself.
  • Loose coupling means a class depends on an abstraction and receives the dependency from outside.

Lower coupling is not automatically better in every line of code, but tighter coupling always raises the cost of change.

Tight coupling example

Here’s a checkout flow that directly creates its own payment dependency:

class StripeGateway {
void charge(int cents) {
System.out.println("Charging Stripe: " + cents);
}
}
class CheckoutService {
private final StripeGateway gateway = new StripeGateway();
void checkout(int totalInCents) {
gateway.charge(totalInCents);
System.out.println("Order completed");
}
}

This works, but it creates three problems immediately:

  • CheckoutService cannot be tested without touching the real payment implementation
  • replacing Stripe with another provider requires changing the class itself
  • any setup logic inside StripeGateway becomes unavoidable in every test

Loose coupling example

Now the same behavior, but with an abstraction:

interface PaymentGateway {
void charge(int cents);
}
class StripeGateway implements PaymentGateway {
public void charge(int cents) {
System.out.println("Charging Stripe: " + cents);
}
}
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
void checkout(int totalInCents) {
gateway.charge(totalInCents);
System.out.println("Order completed");
}
}

Now CheckoutService only knows what it needs: something that can charge().

That opens the door to simple testing:

import static org.junit.jupiter.api.Assertions.assertTrue;
class FakeGateway implements PaymentGateway {
boolean charged;
@Override
public void charge(int cents) {
charged = true;
}
}
FakeGateway fakeGateway = new FakeGateway();
CheckoutService service = new CheckoutService(fakeGateway);
service.checkout(5000);
assertTrue(fakeGateway.charged);

No network calls. No real payment provider. No test environment gymnastics.

Constructor injection is usually enough

Teams sometimes hear “loose coupling” and jump straight into factories, service locators, and abstraction layers everywhere. Most of the time, simple constructor injection gets you 80% of the benefit with almost none of the ceremony.

When tight coupling is acceptable

Not every class needs an interface.

Tight coupling is usually fine when:

  • the class is a tiny value object with no external dependency
  • the dependency is part of the language or JDK and extremely stable
  • there is genuinely no alternate behavior and no testing penalty

The mistake is not using tight coupling sometimes. The mistake is using it by default in code that will evolve.

Why this matters in automation code

This shows up constantly in test frameworks:

  • page objects that instantiate drivers or API clients themselves
  • utility classes that reach into static global state
  • report managers that depend on one concrete reporting tool everywhere
  • config classes that read files directly in every method

The result is brittle setup, hard-to-isolate tests, and code that becomes painful to parallelize. A lot of so-called framework complexity is really just coupling debt.

You can see the opposite pattern in frameworks that got the abstraction boundary right. One reason Playwright’s API feels lighter than many Selenium stacks is that it removed a lot of the wrapper indirection teams used to build by hand. I wrote more about that in my migration notes on Selenium wrappers vs Playwright locators.

A simple rule I use

If a dependency might change, might need to be faked in tests, or might behave differently across environments, I want loose coupling.

If it is pure, stable, and local, I keep it simple.

Where teams overdo it

I do not create interfaces for every helper class just to look “clean.” If there is only one stable implementation and no meaningful test benefit from abstraction, adding an interface is paperwork, not architecture.

Bottom line

Loose coupling is not about writing more interfaces to look architectural. It’s about keeping behavior swappable and testable. When a class depends on abstractions instead of concrete details, change gets cheaper, tests get smaller, and the design survives longer.

That is the practical payoff. Not elegance for its own sake, but code you can work on without fighting it.

§ Further Reading 03 of 03
01Automation

The Browser Errors Your Test Suite Never Catches

Your UI tests pass green while the console throws errors. Learn to catch JavaScript and page errors in Selenium and Playwright Java — before users do.

Read →
02Automation

Managing UI Text in Test Automation

When to use properties files, enums, or spreadsheets for UI text in test automation, and how to keep assertions maintainable across locales and releases.

Read →
03Automation

Thread Safety in Parallel Tests: The 3-Day Bug

How a shared WebDriver instance caused phantom failures in our 800-test parallel suite, and the ThreadLocal pattern that finally stabilized the whole run.

Read →

Don't miss a thing

Subscribe to get updates straight to your inbox.

HT

No spam · Unsubscribe anytime

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.