Sign inSign up

onyxwizard/tradeforge

By onyxwizard

β€’Updated 2 months ago

builds a synchronous monolith using real tools to set a baseline for future upgrades.

Image
Message queues
API management
Web servers
0

104

onyxwizard/tradeforge repository overview

β πŸ—οΈ TradeForge β€” Stageβ€―1: The Monolith (Real Integrations)

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.


β πŸ“– Overview

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:

  • Validates input
  • Calculates totals from real product prices stored in PostgreSQL
  • Charges the customer via the Stripe Mock HTTP API (simulating Stripe)
  • Decreases inventory inside a database transaction (with row‑level locking)
  • Saves the order record
  • Generates a real PDF invoice using pdfkit
  • Sends a real confirmation email through MailHog (SMTP)
  • Creates a shipping record

The 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.


⁠🧱 Architecture (Layered Monolith with External Services)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         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.


β πŸ”§ Technology Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkExpress 4
DatabasePostgreSQL 15 (Alpine)
Payment Gatewaystripe-mock (official Stripe mock server)
Email TestingMailHog (SMTP + web UI)
PDF Generationpdfkit
Email Sendingnodemailer
Container OrchestrationDocker & Docker Compose
Load Testingautocannon

All integrations are real: there are no setTimeout fakes inside the monolith. Network latency, database transactions, file I/O, and SMTP communication are genuine.


β πŸš€ Running the Project

⁠Prerequisites
  • Docker Desktop (or Docker Engine + Docker Compose)
  • curl or any API client
  • Node.js 20 (if you want to run the monolith locally without Docker)
⁠1. Clone and start
git clone <your-repo-url>
cd tradeforge
docker compose up --build

This starts four containers: tradeforge-db (PostgreSQL), stripe-mock, mailhog, and tradeforge-monolith.

⁠2. Verify the setup

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
⁠3. Place a test order
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"
}
⁠4. Run the load test
# 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

β πŸ“Š Performance Baseline (Stageβ€―1)

Load test configuration: 10 concurrent connections, 10 seconds, with enough stock to avoid 409 Conflict responses.

MetricValue
Average latency~1807 ms *
Max latency2670 ms
Successful orders8 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.


⁠⚠️ Known Architectural Smells

  1. Tight coupling – The order service directly imports and calls every other domain (payment, inventory, invoice, email, shipping). Changing one often requires modifying the orchestrator.
  2. Blocking I/O on the critical path – Invoice generation and email sending are non‑critical from the user’s perspective but still delay the HTTP response.
  3. Poor fault isolation – If the invoice generator throws an error (e.g., disk full), the whole order fails, even though payment succeeded and stock was reduced. The current code does not yet implement compensating transactions (refunds).
  4. Limited scalability – Scaling the monolith horizontally duplicates all the heavy processing; the only way to speed up individual requests is to scale up the single process.
  5. Mixed error responses – The monolith returns 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.


β πŸ“ Project Structure (Stageβ€―1)

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

β πŸ“ˆ Roadmap (this is Stageβ€―1 of 3)

  • Stageβ€―1 (this page) – Monolith with layered architecture, real external integrations, synchronous processing.
  • Stageβ€―2 – Event‑driven microservices: split into Order, Payment, Inventory, Invoice, Email, Shipping services connected via RabbitMQ. The POST /orders endpoint returns immediately; all heavy work happens asynchronously in the background.
  • Stageβ€―3 – CQRS: introduce Redis as a read‑optimised cache, project events into denormalised views, and build an ultra‑fast order dashboard.

⁠🀝 Contributing

This is a personal learning project built to understand backend architectural evolution. Feedback, suggestions, and pull requests are welcome.


β πŸ“„ License

MIT β€” feel free to use this project for your own learning.

Tag summary

Content type

Image

Digest

sha256:0a424c359…

Size

69.4 MB

Last updated

2 months ago

docker pull onyxwizard/tradeforge