The brain
The brain makes decisions: Is this order valid for the current menu? Does the customer get a discount? Can this order still be cancelled? In a microservice, the brain is the business logic — the reason the service exists.
The brain shouldn't touch the world directly
Your brain doesn't directly move your hand; it sends signals and the hand acts. Good services work the same way: the brain decides, and adapters (senses, memory, voice, hands) handle the outside world.
This is often called hexagonal architecture or ports and adapters:
senses (API, events, queues)
│
▼
hands ◄──── 🧠 BRAIN (pure logic) ────► voice (events)
│
▼
memory (database)
The brain defines what it needs ("save an order", "announce an event") as simple interfaces. Adapters provide the real implementations using AWS.
A brain in code
// brain/place-order.ts — business rules only; no AWS SDK imports
import { randomUUID } from "node:crypto";
import type { PlaceOrder } from "../skin/place-order.schema";
export interface Menu { priceOf(menuItemId: string): Promise<number | undefined>; }
export interface OrderMemory { save(order: Order): Promise<void>; }
export interface Voice { announce(type: "OrderPlaced", detail: Order): Promise<void>; }
export type Order = {
orderId: string;
tableNumber: number;
items: { menuItemId: string; quantity: number; price: number }[];
total: number;
status: "PLACED";
placedAt: string;
};
export async function placeOrder(input: PlaceOrder, deps: { menu: Menu; memory: OrderMemory; voice: Voice }) {
const items = [];
for (const item of input.items) {
const price = await deps.menu.priceOf(item.menuItemId);
if (price === undefined) throw new Error(`Unknown menu item: ${item.menuItemId}`);
items.push({ ...item, price });
}
const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const order: Order = {
orderId: randomUUID(),
tableNumber: input.tableNumber,
items,
total,
status: "PLACED",
placedAt: new Date().toISOString(),
};
await deps.memory.save(order);
await deps.voice.announce("OrderPlaced", order);
return order;
}
Notice what's not here: no DynamoDB client, no EventBridge client, no HTTP parsing. That makes the brain:
- Easy to test — pass fake memory and voice, check the decisions.
- Easy to change — swap DynamoDB for something else without touching the rules.
- Easy to read — the business rules aren't buried in plumbing.
Testing the brain
// test/place-order.test.ts
import { placeOrder } from "../src/brain/place-order";
test("calculates the total from menu prices", async () => {
const saved: any[] = [];
const announced: any[] = [];
const order = await placeOrder(
{ tableNumber: 4, items: [{ menuItemId: "chai", quantity: 2 }] },
{
menu: { priceOf: async () => 40 },
memory: { save: async (o) => { saved.push(o); } },
voice: { announce: async (_t, o) => { announced.push(o); } },
}
);
expect(order.total).toBe(80);
expect(saved).toHaveLength(1);
expect(announced).toHaveLength(1);
});
How big should a brain be?
One service's brain should cover one area of knowledge — what domain-driven design calls a bounded context. The Waiter's brain knows about orders and tables. It doesn't know how to cook (the Chef's brain) or how card payments work (the Cashier's brain). If a brain starts learning everything, you're growing a monolith again.
Common brain disorders
| Symptom | Cause | Treatment |
|---|---|---|
| Business rules scattered across handlers | Senses doing the thinking | Move logic into the brain module |
| Can't test without AWS | Brain calling the SDK directly | Introduce interfaces (ports) |
| Same rule implemented in three services | Unclear ownership | Give the rule to one human; others ask or listen |
| Huge, slow functions | Too many responsibilities | Split by use case, or split the service |
Keep the brain pure: inputs in, decisions out, and the outside world reached only through adapters. It's the single best habit for maintainable microservices.