builds a synchronous monolith using real tools to set a baseline for future upgrades.
104
A learning project that evolves a single codebase from a monolithic application through eventβdriven microservices to CQRS.
Stageβ―1 is the starting point: a synchronous Node.js/Express application that talks to real external services (Stripe Mock, MailHog, PostgreSQL) and uses real libraries for PDF generation and email delivery. Everything runs locally via Docker Compose.
This is the Stageβ―1 implementation of a productionβlike eβcommerce order backend. The entire business logic lives in one process (the monolith), which orchestrates every step of order placement synchronously:
pdfkitThe user waits for all these actions to complete before receiving an HTTP response. The goal is to establish a realistic performance baseline and to experience firstβhand the coupling and blocking I/O that motivate a move to eventβdriven microservices.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Compose β
β β
β βββββββββββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β β tradeforge-monolith β β stripe-mock β β mailhog β β
β β (Node.js/Express) β β (HTTP API) β β (SMTP/Web UI)β β
β βββββββββββ¬ββββββββββββ ββββββββ¬ββββββββ βββββββββ¬ββββββββ β
β β β β β
β β HTTP POST /charge β β SMTP β
β βββββββββββββββββββββββΊβ β β
β β β βββββββββββ
β β β β β
β β β β β
β β βββββββββββββββββββ΄ββββββββββββββββββββββ β
β β β tradeforge-db (PostgreSQL 15) β
β ββββββ€ Tables: products, orders, shipments β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Inside the tradeforge-monolith container:
Client
β POST /api/v1/orders
βΌ
Express Server
βββ routes/orderRoutes.js
βββ controllers/orderController.js
βββ services/orderService.js
βββ inventoryService.js (real DB queries, transactions, row locks)
βββ paymentService.js (real HTTP call to stripe-mock)
βββ invoiceService.js (real PDF generation with pdfkit)
βββ emailService.js (real SMTP email via nodemailer β mailhog)
βββ shippingService.js (inserts shipment record in DB)
βββ repositories/
βββ orderRepository.js
βββ shipmentRepository.js
All business logic follows a layered architecture (Controller β Service β Repository).
Every step is blocking β the HTTP request does not return until all actions are complete.
| Layer | Technology |
|---|---|
| Runtime | Node.js 20 (Alpine) |
| Framework | Express 4 |
| Database | PostgreSQL 15 (Alpine) |
| Payment Gateway | stripe-mock (official Stripe mock server) |
| Email Testing | MailHog (SMTP + web UI) |
| PDF Generation | pdfkit |
| Email Sending | nodemailer |
| Container Orchestration | Docker & Docker Compose |
| Load Testing | autocannon |
All integrations are real: there are no setTimeout fakes inside the monolith. Network latency, database transactions, file I/O, and SMTP communication are genuine.
curl or any API clientgit clone <your-repo-url>
cd tradeforge
docker compose up --build
This starts four containers: tradeforge-db (PostgreSQL), stripe-mock, mailhog, and tradeforge-monolith.
Check that the database, mock payment, and mail catcher are healthy:
# PostgreSQL
docker exec -it tradeforge-db psql -U tradeforge -d tradeforge -c "SELECT count(*) FROM products;"
# Stripe Mock
curl http://localhost:12111/
# MailHog UI
open http://localhost:8025
curl -X POST http://localhost:3000/api/v1/orders \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"items": [
{"productId": 1, "quantity": 1},
{"productId": 2, "quantity": 2}
],
"email": "[email protected]"
}'
Success response (201 Created):
{
"order": {
"id": 2,
"user_id": 1,
"items": [{"productId": 1, "quantity": 1}, {"productId": 2, "quantity": 2}],
"total_amount": "1039.97",
"status": "CONFIRMED",
"created_at": "2026-07-08T17:38:08.835Z"
},
"paymentId": "pi_R0E7NV8rlKu4LZY",
"invoicePath": "/app/pdfs/invoice-2.pdf",
"trackingNumber": "SHIP-E33TFVP4"
}
# Install autocannon globally (if not already)
npm install -g autocannon
# Execute the test (adjust product ID / quantity to avoid stockβout)
autocannon -c 10 -d 10 \
-m POST \
-H "Content-Type: application/json" \
-b '{"userId":1,"items":[{"productId":1,"quantity":1}],"email":"[email protected]"}' \
http://localhost:3000/api/v1/orders
Load test configuration: 10 concurrent connections, 10 seconds, with enough stock to avoid 409 Conflict responses.
| Metric | Value |
|---|---|
| Average latency | ~1807 ms * |
| Max latency | 2670 ms |
| Successful orders | 8 out of 58 attempts |
| Requests per second | ~5.8 req/s |
*The average is inflated by many fast failures when stock was exhausted. Pure successful orders show latency in the 2β2.6 second range. After ensuring unlimited stock, clean measurements show an average latency of X ms (to be filled after a clean allβ2xx run).
Why is it slow?
Each successful request must sequentially:
- Make an HTTP call to stripeβmock (network latency + processing)
- Execute a multiβrow database transaction with row locks
- Generate a full PDF invoice (file I/O)
- Send an SMTP email to MailHog (network I/O)
- Insert a shipment record
All these steps happen on the critical path, leaving the user waiting for the slowest operation.
400 for almost all errors, making it hard to distinguish between validation failures, stock shortages, and server outages.These pain points directly motivate the transition to Stageβ―2: EventβDriven Microservices, where RabbitMQ decouples the services and the HTTP endpoint becomes a fast fireβandβforget operation.
tradeforge/
βββ docker-compose.yml # Defines all containers (monolith, DB, stripe-mock, mailhog)
βββ scripts/
β βββ seed-db.sql # Creates products, orders, shipments tables + sample data
β βββ seed-shipments.sql # Optional: separate shipping schema
βββ monolith/
β βββ Dockerfile
β βββ package.json
β βββ .env
β βββ src/
β βββ server.js
β βββ config/
β β βββ db.js # pg Pool + initDb
β βββ controllers/
β β βββ orderController.js
β βββ services/
β β βββ orderService.js # Orchestrator
β β βββ paymentService.js # Stripe SDK β stripe-mock
β β βββ inventoryService.js # Transactions + FOR UPDATE
β β βββ invoiceService.js # pdfkit PDF generation
β β βββ emailService.js # nodemailer β MailHog
β β βββ shippingService.js # Creates shipment record
β βββ repositories/
β β βββ orderRepository.js
β β βββ shipmentRepository.js
β βββ routes/
β βββ orderRoutes.js
βββ docs/
βββ STAGE1.md # This documentation
POST /orders endpoint returns immediately; all heavy work happens asynchronously in the background.This is a personal learning project built to understand backend architectural evolution. Feedback, suggestions, and pull requests are welcome.
MIT β feel free to use this project for your own learning.
Content type
Image
Digest
sha256:0a424c359β¦
Size
69.4 MB
Last updated
2 months ago
docker pull onyxwizard/tradeforge