Skip to main content
Opinix Trade Architecture Diagram

System architecture

Opinix Trade is a real-time opinion trading platform built as a monorepo using Turborepo. The architecture is designed to handle high-frequency order processing with asynchronous workflows and real-time updates.
The system uses a queue-based architecture to handle order matching asynchronously, ensuring the main API remains responsive even under heavy load.

Core components

The platform consists of five main components working together:
1

Client (Next.js)

Frontend application where users place orders and receive real-time updates via WebSocket connections.
2

Backend API (Express)

RESTful API server that validates orders and pushes them to the Redis queue for async processing.
3

Queue (Redis)

Message broker that stores pending orders and enables async communication between services.
4

Worker (Engine)

Background worker that processes orders from the queue using the matching engine to execute trades.
5

WebSocket Server

Real-time server that broadcasts order book updates and trade events to all connected clients.

Technology stack

Frontend

  • Next.js 14 (App Router)
  • TypeScript
  • Server Actions

Backend

  • Express.js
  • Node.js
  • TypeScript

Real-time

  • WebSocket (ws)
  • Redis Pub/Sub
  • Subscription Manager

Infrastructure

  • Redis (Queue & Cache)
  • PostgreSQL (Database)
  • Turborepo (Monorepo)

Data flow

  1. User submits an order through the Next.js client
  2. Frontend calls the backend API /order/initiate endpoint
  3. Backend validates the order and pushes it to the ORDER_QUEUE in Redis
  4. Backend immediately responds to the client with “Order placed successfully”
  5. Worker polls the queue and processes the order using the Engine
  6. Engine executes the matching algorithm and updates balances
  7. Engine publishes updates to Redis channels for WebSocket distribution
  8. WebSocket server broadcasts updates to all subscribed clients
  1. Clients subscribe to specific channels (e.g., depth@bitcoin-event, trade@bitcoin-event)
  2. Engine publishes messages to Redis channels after order execution
  3. WebSocket server’s SubscriptionManager receives messages via Redis Pub/Sub
  4. WebSocket server broadcasts messages only to clients subscribed to that channel
  5. Clients receive updates and re-render the order book UI

Monorepo structure

The project uses Turborepo with the following workspace organization:
opinix-trade/
├── apps/
   ├── client/          # Next.js frontend
   └── server/          # Express backend API
├── packages/
   ├── db/              # Database client (Prisma)
   ├── logger/          # Shared logging utility
   ├── order-queue/     # Redis queue manager
   ├── types/           # Shared TypeScript types
   └── zod/             # Validation schemas
└── services/
    ├── engine/          # Matching engine worker
    └── wss/             # WebSocket server
All packages are written in TypeScript and share types from @opinix/types to ensure type safety across the entire system.

Key architectural decisions

Async order processing

Orders are processed asynchronously using a Redis queue to decouple the API from the computationally expensive matching logic. This ensures:
  • Fast API responses: The API responds immediately without waiting for order execution
  • Scalability: Multiple workers can process orders in parallel
  • Reliability: Orders are persisted in Redis and won’t be lost if a worker crashes

Real-time updates via WebSockets

The WebSocket server uses a subscription-based model where clients subscribe to specific event channels:
// From services/wss/src/classes/SubscriptionManager.ts:37
subscribe(userId: string, subscription: string) {
  if (this.subscriptions.get(userId)?.includes(subscription)) {
    return;
  }
  const newSubscription = (this.subscriptions.get(userId) || []).concat(subscription);
  this.subscriptions.set(userId, newSubscription);

  // reverseSubscription for O(1) lookup
  const newRevSubscription = (this.reverseSubscriptions.get(subscription) || []).concat(userId)
  this.reverseSubscriptions.set(subscription, newRevSubscription)

  if (this.reverseSubscriptions.get(subscription)?.length === 1) {
    this.redisClient.subscribe(subscription, this.redisCallbackHandler);
  }
}
The reverse subscription pattern is critical for performance. It allows O(1) lookup when broadcasting messages to subscribed users.

In-memory order book

The Engine maintains the order book entirely in memory for maximum performance. It periodically snapshots state to disk:
// From services/engine/src/trade/Engine.ts:47
setInterval(() => {
  this.saveSnapshot();
}, 1000 * 3);

Next steps