Mental Model & Philosophy

Deployment Flexibility

Same code in monolith, microservices, serverless, edge

PURISTA services are infrastructure-agnostic. The same business logic runs on a laptop, in a Docker container, on Kubernetes, or as a serverless function. The only thing that changes is the event bridge adapter and bootstrap configuration.

This is not a future-proofing trick — it is how you move from a prototype to a production system without rewriting anything. Start with the DefaultEventBridge in a single process on day one. When the team grows or scaling demands separate, swap the bridge and deploy independently. Your commands and subscriptions never change.

Deployment patterns at a glance

PatternArchitectureBest forComplexity
MonolithAll services in one processFastest delivery, smallest ops overheadLow
MicroservicesOne service per process/containerIndependent release cycles, team autonomyMedium
Serverless / FaaSFunction-per-triggerBursty workloads, platform-managed scalingMedium
EdgeLightweight single-processIoT, on-device, constrained environmentsLow

Deployment decision tree

flowchart TD
    A["Start here"] --> B{"Team size?"}
    B -->|Small, one team| C["Monolithic"]
    B -->|Multiple teams| D{"Release independence needed?"}
    D -->|No| C
    D -->|Yes| E["Microservice"]
    A --> F{"Workload pattern?"}
    F -->|Bursty, sporadic| G["Serverless / FaaS"]
    F -->|Continuous, low latency| H{"Environment?"}
    H -->|Cloud / Data center| E
    H -->|Edge / Device| I["Edge"]

Monolith — start here

All services share one process and one in-memory event bridge:

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()

console.log('All services running. Press Ctrl+C to stop.')

Why start with a monolith?

  • No broker setup required
  • Fastest local development and debugging
  • Single deploy target
  • Easy to refactor service boundaries

Microservices — split when ready

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,
  exchangeName: 'purista',
})

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,
  exchangeName: 'purista',
})

const emailService = await emailV1Service.getInstance(eventBridge)
await emailService.start()

The service code does not change. Only the bootstrap file changes.

Serverless — function-per-command

Services run in serverless environments using the in-memory DefaultEventBridge — no broker connection required per invocation. Expose commands via the HTTP server and let the platform manage scaling:

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()
// The HTTP server handles incoming requests; the event bridge routes them in-process

Best for: bursty workloads, sporadic traffic, pay-per-invocation models.

Edge — lightweight and local

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()

Best for: IoT, on-device processing, constrained environments.

Scaling model

Because PURISTA services are stateless, scaling is horizontal:

flowchart LR
    LB["Load Balancer<br/>or Broker"] --> I1["Instance 1"]
    LB --> I2["Instance 2"]
    LB --> I3["Instance 3"]
    I1 --> DB[(Database)]
    I2 --> DB
    I3 --> DB
  • The broker distributes messages across service instances
  • No session affinity required
  • Instances are interchangeable — start more, stop some, no data loss
  • Scale per service — User Service needs 3 instances, Email Service needs 1

Runtime configuration

EnvironmentEvent BridgeQueue BridgeStore
Local devDefaultEventBridgeDefaultQueueBridgeDefaultStateStore
CI / testingDefaultEventBridgeDefaultQueueBridgeDefaultStateStore
StagingAmqpBridge or NatsBridgeRedisQueueBridgeRedisStateStore
ProductionAmqpBridge or NatsBridgeRedisQueueBridge or NatsQueueBridgeRedisStateStore or NatsStateStore
ServerlessDefaultEventBridgeRedisQueueBridgeRedisStateStore
EdgeMqttBridgeMQTT-nativeDaprStateStore (@purista/dapr-sdk)

When to migrate between models

FromToSignal
MonolithMicroservicesMultiple teams need independent deploys
MonolithServerlessSporadic traffic, cost optimization
MicroservicesMonolithOps overhead exceeds team capacity
AnyEdgeLatency requirements demand local processing

Common pitfalls

  • Designing for microservices too early. Start with a monolith. Extract services when boundaries are clear and teams are ready.
  • Hardcoding bridge configuration. Use environment variables and config stores for broker URLs, credentials, and timeouts.
  • Ignoring cold starts. Serverless functions have startup latency. Pre-warm critical paths.
  • Assuming shared memory. Even in a monolith, services should not share state. Use stores.

Checklist

  • The same service code runs in at least two deployment models
  • Event bridge configuration is externalized (env vars, config stores)
  • Graceful shutdown is implemented for all deployment targets
  • Health checks are exposed and monitored
  • Secrets are in secret stores, not environment variables or code
  • Integration tests pass against the target broker/store setup
  • Scaling strategy is documented (horizontal, per-service, auto-scaling)

Related

Read Next
What is a Service?

from Service — The Container