{
  "slug": "When-Your-Domain-Talks-to-Stripe--A-DDD-Cautionary-Tale-9b498a851a7d",
  "title": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
  "subtitle": "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.",
  "excerpt": "A real-world dissection of a circular dependency in a billing module, and how Domain-Driven Design gives you the vocabulary — and the tools…",
  "date": "2026-07-25",
  "tags": [
    "DDD",
    "Architecture",
    "Stripe",
    "Payments"
  ],
  "readingTime": "6 min",
  "url": "https://blog.devops.dev/when-your-domain-talks-to-stripe-a-ddd-cautionary-tale-9b498a851a7d",
  "hero": "https://miro.medium.com/v2/resize:fit:1200/1*DHVw0VkL48Yuik_FO3lV9A.png",
  "content": [
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "Every senior engineer has seen it. A <code>forwardRef()</code> buried in a module definition, added months ago by someone who needed to &quot;just make the circular dependency go away.&quot; 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."
    },
    {
      "type": "paragraph",
      "html": "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 <em>name</em> what went wrong and know exactly how to fix it."
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*DHVw0VkL48Yuik_FO3lV9A.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 2752,
      "height": 1536
    },
    {
      "type": "quote",
      "html": "“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.”"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "What is Domain-Driven Design?"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "DDD gives us a shared language. Here are the terms you need to understand the rest of this article:"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*x4X8182wul3omDAQaJUS1Q.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 568,
      "height": 771
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The building blocks: Layers, Ports, and Adapters"
    },
    {
      "type": "paragraph",
      "html": "A well-structured DDD application has three layers. Each layer is only allowed to depend on the layers inside it — never outward."
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*qDIPiAgZ5lnQ0a2QJk1m9A.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 569,
      "height": 391
    },
    {
      "type": "paragraph",
      "html": "This arrangement is sometimes called Hexagonal Architecture or Ports &amp; 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."
    },
    {
      "type": "paragraph",
      "html": "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 <code>BillingService</code>. You can run the billing domain in a CI environment with no credentials at all."
    },
    {
      "type": "quote",
      "html": "<strong>The Dependency Rule </strong>Source code dependencies must only point inward — toward the domain. The domain layer must have zero compile-time knowledge of any infrastructure class."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The mistake: when infrastructure leaks into the domain"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "Here is what it looks like:"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "// ❌ The domain imports an infrastructure class directly\nimport { StripeService } from '@/lib/modules/stripe/stripe.service';\n\n@Injectable()\nexport class BillingService {\n  constructor(\n    @Inject(forwardRef(() => StripeService))\n    private readonly stripeService: StripeService,\n\n  ) {}\n\n  async purchasePackage(dto, user) {\n    // The domain is calling Stripe directly\n    const customerId = await this.stripeService.syncUser(user);\n    return this.stripeService.createCheckoutSession(dto);\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "And here is the same pattern in <code>InvoiceService</code>:"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "// ❌ Same violation in a second domain service\nimport { StripeService } from '@/lib/modules/stripe/stripe.service';\n\n@Injectable()\nexport class InvoiceService {\n  constructor(\n    @Inject(forwardRef(() => StripeService))\n    private readonly stripeService: StripeService,\n\n  ) {}\n}"
    },
    {
      "type": "paragraph",
      "html": "And on the other side, the Stripe webhook handler — living inside <code>StripeModule</code> — calls back into both domain services:"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "// ❌ Infrastructure calls into the domain — the cycle closes here\nvoid this.billingService.renewSubscription(dto);\nvoid this.billingService.pauseUserSubscription(id);\nvoid this.invoiceService.create(createInvoiceDto);\nvoid this.invoiceService.updateStatus(invoice.id, 'paid');"
    },
    {
      "type": "paragraph",
      "html": "The result is a closed loop — a cycle NestJS cannot resolve without a hint:"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*w73XHp-AxcIutj0SUGjDQQ.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 557,
      "height": 151
    },
    {
      "type": "paragraph",
      "html": "<code>forwardRef()</code> 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 <code>forwardRef</code> is normal in this codebase."
    },
    {
      "type": "quote",
      "html": "<strong>Why forwardRef is a red flag, not a solution </strong>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."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Diagnosing our billing module"
    },
    {
      "type": "paragraph",
      "html": "Let us be precise about what DDD principle is violated and why it matters."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The Dependency Inversion Principle"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "When <code>BillingService</code> imports <code>StripeService</code> by class name, it is binding the billing rules — the most important code in the system — to a specific third-party library. That library&#39;s interface, upgrade cycle, error types, and test requirements all become the billing domain&#39;s problem."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "What coupling actually costs"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*C0u77sGOdrTbzM8SZSvyxw.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 570,
      "height": 476
    },
    {
      "type": "heading",
      "level": 2,
      "text": "The fix: inverting the dependency"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Define the port in the domain layer"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Inject the port symbol into domain services"
    },
    {
      "type": "paragraph",
      "html": "BillingService and InvoiceService receive an <code>IPaymentGateway</code> — not a <code>StripeService</code>. They have no idea who is on the other end."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Create the adapter in the infrastructure layer"
    },
    {
      "type": "paragraph",
      "html": "StripePaymentGatewayAdapter implements IPaymentGateway. It knows everything about Stripe. The domain knows nothing about this class."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Move the webhook handler to an application-layer module"
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "<strong>The resulting dependency graph is a strict directed acyclic graph (DAG). Every arrow points in one direction. <code>forwardRef</code> disappears from the codebase entirely.</strong>"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*YTu1bVBzHW-oMQzY6_nDzg.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 571,
      "height": 116
    },
    {
      "type": "quote",
      "html": "<strong>What you gain</strong>BillingService 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."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Rules of thumb to carry forward"
    },
    {
      "type": "paragraph",
      "html": "DDD is not a checklist — it is a set of instincts. Here are the ones this case study teaches most clearly:"
    },
    {
      "type": "image",
      "src": "https://miro.medium.com/v2/resize:fit:1400/1*XduxcETM3VeMILZdZ83yFg.png",
      "alt": "When Your Domain Talks to Stripe, A DDD Cautionary Tale",
      "caption": "",
      "width": 556,
      "height": 586
    },
    {
      "type": "quote",
      "html": "<em>“A circular dependency between domain and infrastructure is not a technical problem to patch — it is an architectural signal to invert.”</em>"
    },
    {
      "type": "paragraph",
      "html": "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 <code>forwardRef</code> 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."
    },
    {
      "type": "paragraph",
      "html": "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."
    },
    {
      "type": "paragraph",
      "html": "Tags: Domain-Driven Design, NestJS, Hexagonal Architecture, Ports &amp; Adapters, Dependency Inversion, Stripe"
    }
  ]
}
