Mental Model & Philosophy
Distribution
Distribution implementation and runtime level
Distribution in PURISTA is a runtime decision, not an architectural constraint. The same service code can run in a single process, across multiple containers, or as serverless functions — because distribution is handled by the event bridge, not by your business logic.
The distribution illusion
In traditional systems, distribution is baked into the code:
// ❌ Bad: service imports another service directly
import { emailService } from '../email/service.js'
async function createUser(payload) {
const user = await db.create(payload)
await emailService.sendWelcome(user.email) // direct call
return user
}
In PURISTA, services never call each other directly. They send messages:
// ✅ Good: service sends a message
.setCommandFunction(async function (context, payload) {
const user = await context.resources.db.create(payload)
await context.service.EmailService['1'].sendWelcome({ email: user.email }, {})
return user
})
The context.service proxy sends a command message through the event bridge. Whether the Email Service runs in the same process or a different container is invisible to the caller.
Distribution models
Monolith — single process
All services share one event bridge and run in a single Node.js process:
import { DefaultEventBridge } from '@purista/core'
import { userV1Service } from './services/user.js'
import { emailV1Service } from './services/email.js'
const eventBridge = new DefaultEventBridge()
await eventBridge.start()
const userService = await userV1Service.getInstance(eventBridge)
const emailService = await emailV1Service.getInstance(eventBridge)
await userService.start()
await emailService.start()
| Pros | Cons |
|---|---|
| Fastest local development | Single failure domain |
| No broker required | Cannot scale services independently |
| Simple debugging | All services share the same memory space |
Microservices — one service per container
Each service runs in its own container, connected via a shared broker:
import { AmqpBridge } from '@purista/amqpbridge'
import { userV1Service } from './service.js'
const eventBridge = new AmqpBridge({ url: process.env.AMQP_URL })
const userService = await userV1Service.getInstance(eventBridge)
await userService.start()
import { AmqpBridge } from '@purista/amqpbridge'
import { emailV1Service } from './service.js'
const eventBridge = new AmqpBridge({ url: process.env.AMQP_URL })
const emailService = await emailV1Service.getInstance(eventBridge)
await emailService.start()
| Pros | Cons |
|---|---|
| Independent scaling | Operational complexity |
| Independent deployments | Broker required |
| Team autonomy | Network latency between services |
Serverless — function-per-command
Services run in serverless environments using the in-memory DefaultEventBridge — no broker connection required per invocation. Commands are exposed via the HTTP server and the event bridge routes requests in-process:
import { DefaultEventBridge } from '@purista/core'
import { honoV1Service } from '@purista/hono-http-server'
import { userV1Service } from './service.js'
const eventBridge = new DefaultEventBridge()
await eventBridge.start()
const httpServerService = await honoV1Service.getInstance(eventBridge)
const userService = await userV1Service.getInstance(eventBridge)
await httpServerService.start()
await userService.start()
// All services run in-process; the event bridge routes messages without a broker
| Pros | Cons |
|---|---|
| Platform-managed scaling | Cold start latency |
| Pay-per-invocation | Limited execution time |
| Auto-scaling | State must be externalized |
Edge — lightweight single-process
Run services at the edge with minimal infrastructure:
import { MqttBridge } from '@purista/mqttbridge'
import { sensorV1Service } from './service.js'
const eventBridge = new MqttBridge({
url: process.env.MQTT_URL,
clientId: 'edge-sensor-001',
})
const sensorService = await sensorV1Service.getInstance(eventBridge)
await sensorService.start()
| Pros | Cons |
|---|---|
| Low latency for IoT | Limited compute resources |
| Works offline with local MQTT broker | Minimal persistence options |
| Small footprint | Fewer store adapters available |
How the bridge handles distribution
The event bridge is the distribution boundary. It decides:
- Where to route a message (same process or remote)
- How to deliver it (in-memory queue, broker publish, HTTP call)
- When to retry (immediate, exponential backoff, dead-letter)
flowchart TB
subgraph Local["Local Process"]
S1[User Service]
S2[Email Service]
EB[DefaultEventBridge]
end
subgraph Remote["Remote Container"]
S3[Order Service]
EB2[NatsBridge]
end
subgraph Broker["NATS Broker"]
Q1[Queue: user.*]
Q2[Queue: email.*]
Q3[Queue: order.*]
end
S1 -->|command| EB
EB -->|local route| S2
EB -->|remote publish| Broker
Broker -->|deliver| EB2
EB2 -->|route| S3
When to choose each model
| Model | Choose when… |
|---|---|
| Monolith | Small team, fast delivery, single deploy target |
| Microservices | Multiple teams, independent release cycles, scaling needs |
| Serverless | Bursty workloads, sporadic traffic, platform-managed ops |
| Edge | IoT, on-device processing, constrained environments |
Common pitfalls
- Designing for microservices too early. Start with a monolith. Extract services when boundaries are clear.
- Assuming local calls are free. Even in a monolith, message serialization and routing have overhead.
- Ignoring network partitions. In distributed mode, services can be unreachable. Design for timeouts and retries.
- Sharing state between services. Services must be stateless. Use stores for shared data.
Checklist
- The same service code runs in at least two deployment models without changes
- Event bridge configuration is externalized (not hardcoded)
- Services are stateless — no in-memory caches shared across instances
- Cross-service calls handle timeouts and retries gracefully
- Graceful shutdown works in all deployment models
- Health checks verify bridge connectivity, not just service state