Backend-first reference system for order processing with explicit reliability boundaries. The project demonstrates a modular monolith whose external behavior is driven by REST commands and durable domain events.
- Transactional order creation with atomic stock reservation.
- Transactional Outbox for reliable publication to RabbitMQ.
- Idempotency keys for safe command retries.
- Retry with exponential backoff and a dead-letter state for poison events.
- Simulated payment, compensation and order state transitions.
- PostgreSQL for transactional data, Redis for catalog caching, RabbitMQ for delivery and MongoDB for document-shaped audit history.
- Structured logs, correlation IDs propagated through events, Prometheus metrics and readiness checks.
- Unit tests that run without infrastructure and integration tests that run against the Docker Compose services.
- A small React admin console that exercises the real API; the UI is intentionally secondary to the backend.
React admin ──REST──> Fastify API ──transaction──> PostgreSQL
│ │
│ └── outbox_events
├── Redis catalog cache
└── JWT/RBAC
Worker ──poll──> PostgreSQL outbox ──publish──> RabbitMQ ──consume──> MongoDB audit
│
└── retry / dead-letter after five attempts
The API and worker are separate processes from the start, but the domain modules live in one repository. This keeps local development reproducible while preserving a clear extraction seam if operational load later justifies service decomposition.
Requirements: Node.js 22+, Docker and Docker Compose.
cp .env.example .env
npm install
docker compose up -d postgres redis rabbitmq mongo
npm run dev:apiIn another terminal:
npm run dev:worker
npm run dev:adminThe API is available at http://localhost:3000, the admin console at http://localhost:5173, RabbitMQ management at http://localhost:15672 and metrics at http://localhost:3000/metrics.
The default local credentials are the values from .env.example. Change them before exposing the system outside a local environment.
# Authenticate
TOKEN=$(curl -s http://localhost:3000/auth/login \
-H 'content-type: application/json' \
-d '{"email":"admin@example.com","password":"change-me-now"}' | jq -r .accessToken)
# Create a product
PRODUCT=$(curl -s http://localhost:3000/products \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"sku":"keyboard-01","name":"Mechanical keyboard","priceCents":12990,"stock":10}' | jq -r .id)
# Create an idempotent order; replaying the same request returns the same order
curl -s http://localhost:3000/orders \
-H "authorization: Bearer $TOKEN" \
-H 'idempotency-key: demo-order-001' \
-H 'content-type: application/json' \
-d "{\"items\":[{\"productId\":\"$PRODUCT\",\"quantity\":1}]}"Use POST /orders/:id/payment with {"approved":true} to simulate payment capture, and POST /orders/:id/fulfill after the order is paid. Audit records become visible through GET /audit?orderId=... after the worker consumes the event.
npm run lint
npm run typecheck
npm test
npm run build
npm run test:integration --workspace @commerce/apiIntegration tests require the Compose dependencies and exercise the running API, worker, PostgreSQL, RabbitMQ and MongoDB. The CI workflow starts those dependencies and the application before running the integration suite.
- Stock reservation is performed in the same PostgreSQL transaction as the order and outbox insert. A failed payment compensates the reservation in the same transaction as the status transition.
- A successful database transaction is not enough to guarantee message delivery, so the worker publishes from
outbox_eventsand records the publication result separately. - RabbitMQ delivery is at-least-once. Consumers must be idempotent; the audit consumer uses the event ID as a unique MongoDB key.
- Idempotency keys are scoped by authenticated subject and command name. They are retained indefinitely in this demonstration; a production deployment should add a retention policy based on business replay requirements.
- Redis is a cache, never the source of truth. API behavior remains correct when the cache is unavailable.
- ADR-001: modular monolith with separate worker
- ADR-002: transactional outbox and delivery semantics
- ADR-003: storage responsibilities
MIT. This is a portfolio project intended for study and technical discussion.