← All posts

Decomposing a Payment Gateway Integration: A Real /draft:decompose Walkthrough

A real run on a 14-file track. Eight modules, a circular dependency caught and broken before any code was written, and an LLD with contracts for the two high-complexity modules.

The Bottom Line

Most developers who try /draft:decompose for the first time treat it like a planning tool. It isn't. It's an architectural guardrail that forces module boundaries to exist before your AI assistant gets a chance to invent random tasks. The output isn't a to-do list — it's a dependency graph, an implementation order, and (when complexity is high) a per-module contract.

Below is a real run on a 14-file track that went from a tangled task list to four cleanly-phased module builds, with a circular dependency caught and broken before any code was written.

The Track: "Add Multi-Provider Payment Support"

Existing system: a Node.js checkout service with a single hardcoded Stripe integration. ~6,000 lines, no payment abstraction, retries hardcoded, audit logs sprinkled across handlers. The spec landed in draft/tracks/payments-multi-provider/spec.md:

# Multi-Provider Payment Support

## Problem
Single Stripe integration is blocking enterprise deals
that require Adyen and PayPal. Refund flow is buggy.
Webhooks have no signature verification.

## Acceptance
- Stripe, Adyen, PayPal supported behind one provider interface
- Idempotent charge requests (retry-safe)
- Signed webhook verification per provider
- Refunds work for all three providers
- Full audit trail (who, what, when, response)
- Zero-downtime migration from existing Stripe path

A senior engineer would call this two sprints. Most AI assistants will write a 25-task plan in two minutes that misses three of those concerns.

What Happens If You Skip Decomposition

Run /draft:implement directly on the spec above without decomposition, and you get something like:

- [ ] Task 1.1: Add Stripe adapter
- [ ] Task 1.2: Add Adyen adapter
- [ ] Task 1.3: Add PayPal adapter
- [ ] Task 1.4: Add database column for provider
- [ ] Task 1.5: Add webhook endpoint
- [ ] Task 1.6: Implement signature verification
- [ ] Task 1.7: Add refund logic
- [ ] Task 1.8: Add audit logging
... (17 more tasks)

This plan is buildable, but every task touches every other module. There are no clean test boundaries. The "audit logging" task ends up scattered across 11 files. The "refund logic" task hits the same handler six times across the build. By task 14 the plan starts contradicting tasks 3–7.

The problem isn't the AI. The problem is that nobody drew the modules first.

Running /draft:decompose --lld

The skill loads product.md, tech-stack.md, the track's spec.md, and the existing knowledge graph (draft/graph/module-graph.jsonl). It scans the existing checkout codebase, identifies seven candidate modules, and prints a checkpoint:

═══════════════════════════════════════════════════════════
                   MODULE BREAKDOWN
═══════════════════════════════════════════════════════════

Scope: Track: payments-multi-provider

MODULE 1: provider-adapter
  Responsibility: Translate generic payment requests to
                  provider-specific API calls.
  Files: src/payments/providers/{stripe,adyen,paypal}.ts
  API: ProviderAdapter.charge(), .refund(), .verifySignature()
  Dependencies: provider-types
  Complexity: Medium

MODULE 2: provider-types
  Responsibility: Shared types for charge/refund/webhook payloads.
  Files: src/payments/types.ts
  API: ChargeRequest, ChargeResult, WebhookEvent
  Dependencies: none
  Complexity: Low

MODULE 3: payment-router
  Responsibility: Pick a provider per merchant/region/currency
                  and route the call.
  Files: src/payments/router.ts
  API: PaymentRouter.charge(), .refund()
  Dependencies: provider-adapter, provider-types,
                payment-status-port
  Complexity: High

MODULE 4: webhook-dispatcher
  Responsibility: Verify signature, normalize event, dispatch.
  Files: src/payments/webhook.ts
  API: WebhookDispatcher.handle()
  Dependencies: provider-adapter, provider-types,
                refund-orchestrator
  Complexity: Medium

MODULE 5: refund-orchestrator
  Responsibility: Multi-step refund with reconciliation.
  Files: src/payments/refund.ts, src/payments/refund-state.ts
  API: RefundOrchestrator.start(), .complete()
  Dependencies: payment-router, audit-log
  Complexity: High

MODULE 6: audit-log
  Responsibility: Append-only structured payment audit trail.
  Files: src/payments/audit.ts
  API: AuditLog.record()
  Dependencies: provider-types
  Complexity: Low

MODULE 7: idempotency-store
  Responsibility: Persist client idempotency keys with TTL.
  Files: src/payments/idempotency.ts
  API: IdempotencyStore.claim(), .lookup()
  Dependencies: none
  Complexity: Low

═══════════════════════════════════════════════════════════

Two modules are High complexitypayment-router and refund-orchestrator. That auto-triggers LLD generation. The skill notifies:

One or more modules are High complexity — generating LLD automatically.

I made one edit in dialogue: I asked the skill to make audit-log a hard dependency of every state-changing module rather than treating it as a side concern. The skill regenerated the table and the dependency edges.

The Cycle — and How /draft:decompose Breaks It

When the skill ran dependency analysis (Step 4), it printed:

DEPENDENCY ANALYSIS
─────────────────────────────────────────────────────────
⚠ Circular dependency detected:

  payment-router  ──> webhook-dispatcher
                            │
                            ▼
                     refund-orchestrator
                            │
                            ▼
                     payment-router  (cycle)

Reason:
  - payment-router needs webhook-dispatcher to know
    when async charges settle.
  - webhook-dispatcher delegates refund webhooks to
    refund-orchestrator.
  - refund-orchestrator calls payment-router to
    actually issue the refund.

Proposed break:
  Extract `payment-status-port` (interface module).
  - Both payment-router and webhook-dispatcher depend
    on the port.
  - refund-orchestrator depends only on payment-router.
  - The port is a 30-line file, no business logic.

Apply break? [y/N]

This is the part nobody wants to discover at task 17. I accepted the break. The skill added an eighth module (payment-status-port) and re-ran topological sort.

Generated Mermaid Topology

The track-architecture file got a component diagram. Mermaid lives inline in architecture.md, so when /draft:implement reads context later, it sees the topology before writing code:

flowchart TD
  subgraph "in scope"
    R[payment-router]
    A[provider-adapter]
    W[webhook-dispatcher]
    F[refund-orchestrator]
    P[payment-status-port]
    I[idempotency-store]
    L[audit-log]
    T[provider-types]
  end
  subgraph "existing"
    C[checkout-service]
  end
  subgraph "external"
    S[(Stripe API)]
    Y[(Adyen API)]
    PP[(PayPal API)]
  end

  C --> R
  R --> A
  R --> P
  W --> P
  W --> F
  F --> R
  F --> L
  R --> I
  A --> S
  A --> Y
  A --> PP

And a sequence diagram for the happy-path charge:

sequenceDiagram
  participant C as Checkout
  participant R as payment-router
  participant I as idempotency-store
  participant A as provider-adapter
  participant L as audit-log

  C ->> R: charge(req)
  R ->> I: claim(idempotency_key)
  I -->> R: claimed
  R ->> A: charge(req, provider=stripe)
  A ->> Stripe: POST /charges
  Stripe -->> A: 200 OK
  A -->> R: ChargeResult
  R ->> L: record(charge.success)
  R -->> C: ChargeResult

This isn't decoration. It's input the AI reads before writing handlers.

Implementation Order

1. provider-types         (leaf)
2. idempotency-store      (leaf)
3. audit-log              (leaf)
4. payment-status-port    (leaf, extracted to break cycle)
5. provider-adapter       (depends on: provider-types)
6. payment-router         (depends on: provider-adapter,
                          payment-status-port,
                          idempotency-store, audit-log)
7. webhook-dispatcher     (depends on: provider-adapter,
                          payment-status-port)
8. refund-orchestrator    (depends on: payment-router,
                          audit-log)

Parallel opportunities:
  • Phase 1: provider-types, idempotency-store, audit-log,
             payment-status-port (all independent — 4-way parallel)
  • Phase 3: webhook-dispatcher and refund-orchestrator
             can start in parallel after payment-router lands

Eight modules. Four phases. Two parallel windows. Nothing invented.

The LLD: Contracts for the High-Complexity Modules

Because two modules were marked High, --lld populated §6 of architecture.md. For payment-router:

// §6.1 — payment-router API Contract
interface PaymentRouter {
  /**
   * Pre:  req.idempotency_key is non-empty
   * Pre:  req.amount > 0
   * Post: ChargeResult.id is unique
   * Invariant: idempotent — repeat calls with the same
   *            key return the original ChargeResult
   * Errors:
   *   - ProviderError       (transient, retry w/ backoff)
   *   - SignatureError      (permanent, do not retry)
   *   - IdempotencyConflict (permanent, return original)
   */
  charge(req: ChargeRequest): Promise<ChargeResult>
}

§6.3 documents retry policy per error class — exponential backoff for transient, no retry for permanent, circuit breaker after 5 consecutive transient errors per provider. /draft:implement later compiles tests from these contracts — the AI doesn't get to invent a different error taxonomy mid-build.

plan.md, Restructured

Step 6 of the skill regenerated plan.md with phases mapped to modules:

## Phase 1: Foundation (parallelizable)
- [ ] Task 1.1: provider-types
- [ ] Task 1.2: idempotency-store
- [ ] Task 1.3: audit-log
- [ ] Task 1.4: payment-status-port

## Phase 2: Adapters
- [ ] Task 2.1: provider-adapter (Stripe)
- [ ] Task 2.2: provider-adapter (Adyen)
- [ ] Task 2.3: provider-adapter (PayPal)

## Phase 3: Router
- [ ] Task 3.1: payment-router (charge path)
- [ ] Task 3.2: payment-router (refund path)

## Phase 4: Async (parallelizable)
- [ ] Task 4.1: webhook-dispatcher
- [ ] Task 4.2: refund-orchestrator

Eleven tasks. Each lives inside one module. Each module has a contract. Each module is testable in isolation.

What This Buys You

  • No mid-build surprises. The cycle was caught before code existed. Discovering it at task 17 would have meant redoing four files.
  • Real test boundaries. provider-adapter mocks are stable because the contract is fixed in §6.1. Tests don't churn every time the implementation changes.
  • Parallel work. Two engineers (or two AI passes) implement Phase 1 leaves simultaneously. Same for Phase 4.
  • A reviewable architecture diff. architecture.md is checked in. PRs that change module boundaries change it visibly — reviewers get architecture review for free.

The biggest unlock isn't the diagram or the LLD. It's that /draft:implement later reads architecture.md as context. The AI no longer invents a 25-task plan; it implements one module at a time against a contract that already passed your review.

Try It

# In a Draft-initialized repo with an active track
/draft:decompose --lld

# Or for a project-wide refresh after major refactors
/draft:decompose project

The output is two files: draft/tracks/<id>/architecture.md (or draft/architecture.md for project scope) and a restructured plan.md. Read them. Edit them. Then run /draft:implement.

Decomposition isn't extra paperwork. It's the first thing your AI assistant should be reading.