Production Capstone: Assembling the Full E-Commerce Billing & Subscription Engine
Synthesizing all 13 preceding architectural layers — Domain Entities, Aggregates, Domain Events, Use Cases, Ports & Adapters, CQRS, and the full Prisma persistence stack — into a production-deployed Node.js e-commerce billing engine proves that Clean Architecture and Domain-Driven Design produce a system that is resilient, independently testable, and infinitely extensible.
Backend Clean Architecture & Domain-Driven Design
Production Capstone: Assembling the Clean OOP Billing Engine
Domain objects are autonomous state machines with enforced invariants — they are not passive bags of data passed between controller functions. Across 13 articles, we built a billing engine where this is structurally guaranteed: the Domain layer enforces invariants, the Application layer orchestrates without containing rules, the Infrastructure layer adapts without leaking types, and the Interface layer delivers without owning business logic. The Dependency Rule is enforced by the TypeScript compiler, the Outbox Pattern guarantees event delivery, and the full test suite of 249 behaviors runs in under 9 seconds.
This capstone article assembles everything into a production-ready service: structured logging, health checks, graceful shutdown, Dockerfile, Docker Compose, and CI pipeline. It then reviews every architectural decision across the series — not as a summary, but as a principled retrospective that answers why each decision was made and what the alternatives cost.
1. Structured Logging Across the Four Layers
Logging in a layered architecture must respect the Dependency Rule — the Domain layer cannot import a logger. Instead, logging belongs at the Application and Infrastructure layers, where behavior that warrants a log entry actually occurs.
1.1 The Logger Port
1.2 Logging in the Use Case
2. Health Checks
A production service must expose health check endpoints for the load balancer and orchestrator (Kubernetes readinessProbe / livenessProbe):
3. Graceful Shutdown
A billing service must not drop in-flight requests when a new deployment rolls out. Graceful shutdown drains existing requests before exiting:
4. Dockerfile: Multi-Stage Production Build
The multi-stage build produces a minimal image that contains only the compiled JavaScript, the production node_modules, and the Prisma schema — no TypeScript compiler, no test files, no development dependencies.
5. Docker Compose for Development
6. CI Pipeline
7. The Architectural Retrospective: Every Decision and Its Cost
| Decision | Alternative Considered | Why This Decision | Cost |
|---|---|---|---|
| Four-layer Clean Architecture | Three-tier (controller/service/repository) | Forces the Dependency Rule at a structural level; makes each layer independently testable | Extra boilerplate; steeper learning curve for teams new to DDD |
| Domain Entities with private constructors | Plain TypeScript interfaces | Constructor factories enforce invariants at creation time; illegal states are unrepresentable | More verbose than POJOs; requires reconstitute() factory for persistence |
| Branded primitives for IDs | string everywhere |
Cross-ID assignment is a compile-time error; catches the most common billing bug class | Requires factory functions; less ergonomic in test data construction |
| Integer-cent Money | number for amounts |
Eliminates floating-point precision errors in all arithmetic | Conversion overhead; less readable in logs (4999 vs 49.99) |
| IOrderRepository in the Domain layer | Repository in Application layer | Interface ownership determines the dependency direction; Domain owns what it needs | Slightly unintuitive placement for engineers from the three-tier world |
| Collect-then-dispatch Domain Events | Synchronous dispatch inside domain methods | Events dispatched after DB commit → no phantom events for rolled-back transactions | Eventual consistency — handlers may lag; at-least-once requires idempotency |
| Outbox Pattern | Direct event bus publish post-commit | Eliminates the crash-between-commit-and-publish data loss gap | Extra outbox_events table; background poller process; complexity |
| TypeScript project references | ESLint rules / runtime checks | Compiler enforces layer boundaries — violation is a build failure, not a review comment | Initial setup complexity; tsc --build required in CI |
| InversifyJS for DI | Manual wiring in main.ts |
Declarative binding registry; scope management; test container with one-line swaps | Decorator metadata (reflect-metadata); transpiler configuration |
| CQRS read model | Single Aggregate for reads and writes | Read queries are O(1) indexed SELECT; no Aggregate instantiation overhead | Eventual consistency between write and read models; projector complexity |
| Repository contract tests | Mock-based unit tests | Both InMemory and Prisma must pass the same 12 assertions — prevents silent contract violations | Extra test infrastructure; shared suite maintenance |
8. Where to Go Next
The billing engine built across this series is production-ready for a single-service deployment. The natural next steps for a real-world billing system:
8.1 Saga Pattern for Long-Running Workflows
A PlaceOrder that includes payment, inventory reservation, and shipping scheduling spans multiple external systems. If payment succeeds but inventory reservation fails, the order needs to be cancelled and the payment refunded. This is a Saga — a sequence of local transactions coordinated by compensation events. The infrastructure built here (Domain Events + Outbox Pattern) is the foundation for implementing Sagas.
8.2 Event Store for Full Audit Trail
The Outbox Pattern stores events transiently — they are deleted after publication. An Event Store (e.g., EventStoreDB or a custom append-only events table) retains every event permanently. This enables full audit logs, temporal queries ("what was the state of Order X at timestamp T?"), and event replay to rebuild read models from scratch after schema changes.
8.3 Kafka for Cross-Service Events
The InMemoryEventBus dispatches events within the same process. In a microservices architecture, OrderPlaced must be consumed by the Inventory service and the Notification service — separate deployments. Replacing InMemoryEventBus with a Kafka adapter (at the IEventBus binding in the DI container) requires zero changes to the domain or application layers.
8.4 GraphQL API Layer
Adding a GraphQL API is a new driving adapter in src/interface/graphql/. The resolvers call the same Use Cases with the same Command DTOs. The Application and Domain layers are unchanged — this is the Hexagonal Architecture benefit from Part 7 in full effect.
9. The Series at a Glance
Summary
The billing engine implements one principle, end-to-end: dependencies point inward. The Domain layer has no outward dependencies. The Application layer depends only on the Domain. Infrastructure implements Domain interfaces. The Interface layer calls Application use cases.
Every design decision in the series — private constructors, branded primitives, integer-cent Money, repository ports in the domain layer, collect-then-dispatch events, the Outbox Pattern, TypeScript project references — is a direct consequence of enforcing this single rule at every layer boundary.
The test suite validates 249 behaviors in 8.4 seconds. The architecture allows swapping Stripe for PayPal, Prisma for MongoDB, or PostgreSQL for a local Map — each swap is one binding in one container module, zero changes to Domain or Application.
That is what it means for domain objects to be autonomous state machines with enforced invariants. The architecture makes it structurally true — not aspirationally true.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.