You are building a mini centralized exchange where the backend and engine communicate through Redis queues.
The backend does not perform order matching. It only accepts HTTP requests, sends commands to the engine through Redis, waits for the engine response, and returns the result to the client.
The engine owns the in-memory exchange state:
- balances
- order books
- orders
- fills
Frontend / API Client
|
v
Backend API (Express, port 3000)
|
v
Redis queue: backend-to-engine-broker
|
v
Engine process
|
v
Backend-specific response queue
|
v
Backend API ResponseEvery backend process creates its own response queue:
const RESPONSE_QUEUE = `response-queue-${BACKEND_QUEUE_ID}`;Every message sent from the backend to the engine includes:
correlationIdresponseQueuetypepayload
The engine must reply to message.responseQueue and include the same correlationId.
- TypeScript
- Bun
- Express
- Redis
- Prisma/Postgres
- JWT
- Zod
The backend is mostly complete. You should understand the flow, but you do not need to rewrite it.
Important files:
backend/src/index.ts
backend/src/routes/
backend/src/controllers/
backend/src/store/pending-responses.ts
backend/src/types/
backend/src/utils/engine-client.ts
backend/src/utils/auth.ts
backend/src/db.tsThe backend already:
- starts an Express server
- exposes HTTP routes
- validates request bodies with Zod
- creates users with Prisma
- signs JWTs for signup
- protects exchange routes with JWT auth
- sends engine commands to Redis queue
backend-to-engine-broker - listens on its own response queue
- uses
correlationIdto match engine replies to pending HTTP requests - times out if the engine does not respond
The backend intentionally does not have completed signin logic. Students must implement POST /signin in:
backend/src/controllers/auth-controller.tsDo this before using CHECK-FLOW.md, because the flow check requires a JWT token.
The engine has the Redis flow boilerplate and type definitions, but the exchange logic is intentionally incomplete.
Important files:
engine/src/index.ts
engine/src/store/exchange-store.ts
engine/src/utils/env.tsThe engine already:
- connects to Redis
- listens forever on
backend-to-engine-broker - parses messages from the backend
- calls a TODO function where you must implement request handling
- sends the final response to
message.responseQueue - includes the same
correlationIdin the response
Backend to engine:
interface EngineRequest {
correlationId: string;
responseQueue: string;
type:
| "create_order"
| "get_depth"
| "get_user_balance"
| "get_order"
| "cancel_order";
payload: Record<string, unknown>;
}Engine to backend:
interface EngineResponse {
correlationId: string;
ok: boolean;
data?: unknown;
error?: string;
}These are implemented in the backend and directly use Postgres through Prisma.
Creates a user.
Body:
{
"username": "alice",
"password": "password123"
}Signs in a user and returns a JWT.
This endpoint is intentionally incomplete in the boilerplate. Students must:
- validate the body with the existing auth Zod schema
- find the user by username
- compare the password with the stored hashed password
- return a JWT using the existing
createTokenhelper - return
401for invalid credentials
Body:
{
"username": "alice",
"password": "password123"
}These endpoints are implemented in the backend, but they only work after you complete the engine logic.
All of these require:
Authorization: Bearer <jwt-token>Sends create_order to the engine.
Body:
{
"type": "limit",
"side": "buy",
"symbol": "BTC",
"price": 100,
"qty": 10
}Sends get_depth to the engine.
Sends get_user_balance to the engine.
Sends get_order to the engine.
Sends cancel_order to the engine.
You need to complete signin in the backend and then complete the engine.
Backend task:
backend/src/controllers/auth-controller.tsImplement:
signin
After signin works, use CHECK-FLOW.md to verify the backend → queue → engine → queue → backend flow.
Start from:
engine/src/index.ts
engine/src/store/exchange-store.tsYou must implement the logic for:
create_orderget_depthget_user_balanceget_ordercancel_order
You can organize your code however you want inside the engine/ folder.
Use in-memory data structures only. Persistence is not required for the engine.
The boilerplate gives you these placeholders:
export const BALANCES = new Map<string, Record<string, Balance>>();
export const ORDERBOOKS = new Map<string, OrderBook>();
export const ORDERS = new Map<string, OrderRecord>();
export const FILLS: Fill[] = [];You can keep these or change the internal structure if your implementation is clean and correct.
Implement:
- limit orders
- market orders if time permits
- buy orders matching against lowest asks
- sell orders matching against highest bids
- price-time priority
- partial fills
- remaining limit order quantity resting on the book
- order statuses:
openpartially_filledfilledcancelled
- fills returned with each matched trade
- depth grouped by price level
A buy limit order can match with an ask when:
buyPrice >= askPriceA sell limit order can match with a bid when:
sellPrice <= bidPriceMarket orders should match with the best available opposite side prices until filled or the book is empty.
GET /depth/:symbol should return:
{
symbol: "BTC",
bids: [
{ price: 100, qty: 10 },
{ price: 99, qty: 5 }
],
asks: [
{ price: 110, qty: 4 },
{ price: 111, qty: 6 }
]
}Bids must be sorted highest price first.
Asks must be sorted lowest price first.
For this assignment:
- seed balances in memory when the engine starts or when a user is first seen
- do not persist balances to the database
get_user_balanceshould return balances from engine memory- bonus: lock funds/assets for open limit orders
cancel_order should:
- find the order
- return an error if the order does not exist
- return an error if the order is already filled
- remove remaining resting quantity from the order book
- mark the order as
cancelled
Start Redis and Postgres first.
Create env files:
cp backend/.env.example backend/.env
cp engine/.env.example engine/.envInstall dependencies:
cd backend
bun install
cd ../engine
bun installRun backend:
cd backend
bun run devRun engine in another terminal:
cd engine
bun run devBackend runs on:
http://localhost:3000The engine is a worker process. It listens on Redis queue:
backend-to-engine-brokerYou will be evaluated on:
- correct backend to engine Redis flow
- correct engine response to backend-specific response queue
- correct use of
correlationId - signup/signin using DB
- JWT protection on exchange endpoints
- order matching correctness
- partial fills
- depth sorting
- order status updates
- clean, readable TypeScript code
Focus on correctness and readable code over performance.