A real-world dissection of a circular dependency in a billing module, and how Domain-Driven Design gives you the vocabulary — and the tools — to fix it for good.

Every senior engineer has seen it. A forwardRef() buried in a module definition, added months ago by someone who needed to "just make the circular dependency go away." It works. The tests pass. The app starts. And then, six months later, the billing module is untestable in CI, a payment provider migration becomes a three-week project, and two silent bugs have been quietly mis-billing users for the entire time.

This article uses a real NestJS billing module as a case study to explain Domain-Driven Design from the ground up — not as abstract theory, but as the specific, practical vocabulary that lets you name what went wrong and know exactly how to fix it.

When Your Domain Talks to Stripe, A DDD Cautionary Tale
“The goal of DDD is not to draw boxes on a whiteboard. It is to ensure the rules of your business live in one place — and that place is protected from the details of how you implement it.”

What is Domain-Driven Design?

Domain-Driven Design (DDD) is a software development approach introduced by Eric Evans in 2003. Its central premise is simple: the most important thing in your codebase is the business logic — the rules, processes, and concepts that make your product what it is. Everything else — databases, HTTP, payment processors, email providers — is a detail in service of that logic.

DDD gives us a shared language. Here are the terms you need to understand the rest of this article:

When Your Domain Talks to Stripe, A DDD Cautionary Tale

The most important rule in DDD is also the simplest: dependencies point inward. The infrastructure layer knows about the domain. The domain knows nothing about the infrastructure. Never the reverse.

The building blocks: Layers, Ports, and Adapters

A well-structured DDD application has three layers. Each layer is only allowed to depend on the layers inside it — never outward.

When Your Domain Talks to Stripe, A DDD Cautionary Tale

This arrangement is sometimes called Hexagonal Architecture or Ports & Adapters. The shape of the metaphor is apt: the domain sits at the center of a hexagon, and each face of the hexagon is a port — a defined entry/exit point — backed by an adapter on the outside.

The payoff is profound. You can test the entire domain layer with zero infrastructure. You can swap Stripe for Paddle by writing a new adapter, with no changes to BillingService. You can run the billing domain in a CI environment with no credentials at all.

The Dependency Rule Source code dependencies must only point inward — toward the domain. The domain layer must have zero compile-time knowledge of any infrastructure class.

The mistake: when infrastructure leaks into the domain

The most common DDD violation — and the most seductive — is importing an infrastructure class directly into a domain service. It feels harmless. It’s just one import. It saves you fifteen minutes of writing an interface.

Here is what it looks like:

typescript
// ❌ The domain imports an infrastructure class directly
import { StripeService } from '@/lib/modules/stripe/stripe.service';

@Injectable()
export class BillingService {
  constructor(
    @Inject(forwardRef(() => StripeService))
    private readonly stripeService: StripeService,

  ) {}

  async purchasePackage(dto, user) {
    // The domain is calling Stripe directly
    const customerId = await this.stripeService.syncUser(user);
    return this.stripeService.createCheckoutSession(dto);
  }
}

And here is the same pattern in InvoiceService:

typescript
// ❌ Same violation in a second domain service
import { StripeService } from '@/lib/modules/stripe/stripe.service';

@Injectable()
export class InvoiceService {
  constructor(
    @Inject(forwardRef(() => StripeService))
    private readonly stripeService: StripeService,

  ) {}
}

And on the other side, the Stripe webhook handler — living inside StripeModule — calls back into both domain services:

text
// ❌ Infrastructure calls into the domain — the cycle closes here
void this.billingService.renewSubscription(dto);
void this.billingService.pauseUserSubscription(id);
void this.invoiceService.create(createInvoiceDto);
void this.invoiceService.updateStatus(invoice.id, 'paid');

The result is a closed loop — a cycle NestJS cannot resolve without a hint:

When Your Domain Talks to Stripe, A DDD Cautionary Tale

forwardRef() patches this by deferring the dependency resolution to after construction. But it does not fix the problem — it hides it. The cycle still exists. The coupling is still there. And every new engineer who reads this code learns that forwardRef is normal in this codebase.

Why forwardRef is a red flag, not a solution forwardRef() is a valid escape hatch for genuinely unavoidable circular references (e.g., a parent component referencing itself). When it appears between a domain service and an infrastructure service, it is a symptom of a DDD violation — not a fix for one.

Diagnosing our billing module

Let us be precise about what DDD principle is violated and why it matters.

The Dependency Inversion Principle

DDD is grounded in the Dependency Inversion Principle (the D in SOLID): high-level modules should not depend on low-level modules. Both should depend on abstractions. In our context, the domain (high-level) should not depend on Stripe (low-level). Both should depend on an interface.

When BillingService imports StripeService by class name, it is binding the billing rules — the most important code in the system — to a specific third-party library. That library's interface, upgrade cycle, error types, and test requirements all become the billing domain's problem.

What coupling actually costs

When Your Domain Talks to Stripe, A DDD Cautionary Tale

The fix: inverting the dependency

The solution is to introduce a port — an interface owned by the domain — and an adapter owned by the infrastructure. Let us walk through it step by step.

Define the port in the domain layer

The port is a plain TypeScript interface. It describes what the billing domain needs from a payment provider — in the domain’s own vocabulary. No Stripe types, no Stripe imports.

Inject the port symbol into domain services

BillingService and InvoiceService receive an IPaymentGateway — not a StripeService. They have no idea who is on the other end.

Create the adapter in the infrastructure layer

StripePaymentGatewayAdapter implements IPaymentGateway. It knows everything about Stripe. The domain knows nothing about this class.

Move the webhook handler to an application-layer module

StripeWebhookModule lives at the top of the dependency graph. It imports BillingModule and InvoiceModule. The cycle disappears because StripeModule no longer needs to know about billing at all.

The resulting dependency graph is a strict directed acyclic graph (DAG). Every arrow points in one direction. forwardRef disappears from the codebase entirely.

When Your Domain Talks to Stripe, A DDD Cautionary Tale
What you gainBillingService is now fully unit-testable with a two-line mock. Stripe can be replaced with any payment provider by writing a new adapter. The NestJS module graph resolves without any hinting. The domain and infrastructure can evolve independently.

Rules of thumb to carry forward

DDD is not a checklist — it is a set of instincts. Here are the ones this case study teaches most clearly:

When Your Domain Talks to Stripe, A DDD Cautionary Tale
“A circular dependency between domain and infrastructure is not a technical problem to patch — it is an architectural signal to invert.”

The billing module described in this article was not written carelessly. The original engineer faced a real problem — a webhook handler that needed to call domain services, and domain services that needed to call Stripe. The forwardRef solved the immediate problem. But DDD gives us the vocabulary to see it for what it was: a dependency flowing in the wrong direction, waiting for a port.

The fix took one sprint. The result is a billing domain that can be tested in complete isolation, a fully encapsulated Stripe integration, and a module graph that any new engineer can understand at a glance. That is what Domain-Driven Design is actually for.

Tags: Domain-Driven Design, NestJS, Hexagonal Architecture, Ports & Adapters, Dependency Inversion, Stripe