What is BSB?

BSB is a framework for building microservices that communicate through events. Instead of direct HTTP calls between services, every service publishes and subscribes to events through a central event bus.

The key insight: your business logic should not care about infrastructure. Whether your events go through an in-memory bus, RabbitMQ, or Kafka - your service code stays the same.

Core Principles

Event-Driven

Services communicate exclusively through typed events. No direct coupling between services - just event contracts.

Plugin Architecture

Everything is a plugin: configuration, logging, events, metrics, and your services. Swap implementations without changing code.

Container-First

Designed for Docker and Kubernetes. Mount plugin directories at runtime. Configure through environment variables.

Type-Safe

Define event schemas with BSB types and config schemas with AnyVali. Get compile-time type checking and runtime validation across all services.

How It Works

When BSB starts, it discovers and loads plugins, then orchestrates the service lifecycle:

BSB Runtime
Config
📋 Logging
📊 Metrics
Events Plugin routes events between all service plugins
Service A Your Code
Service B Your Code
Service C Your Code

Plugin Types

BSB has four types of plugins. Service plugins are where you write your business logic. The other three are core infrastructure plugins that power the framework:

  • service-* - Your code goes here. Business logic, API handlers, workers - this is what you build.
  • config-* - Loads configuration (YAML, Consul, etcd, etc.)
  • events-* - Routes events between services (in-memory, RabbitMQ, Kafka, etc.)
  • observable-* - Unified logging, metrics, and tracing (console, Graylog, OpenTelemetry, Pino, etc.)

Event Communication

Services communicate through three event patterns:

Pattern Description Use Case
emitEvent Fire-and-forget. First listener receives. Background jobs, notifications
emitEventAndReturn Request-response. Wait for a result. Validation, data lookup, RPC-style calls
emitBroadcast All listeners receive the event. Cache invalidation, real-time updates

Service Lifecycle

Every service plugin follows a predictable lifecycle:

  1. Constructor - Create class fields, generated service clients, pure helpers, and cheap in-memory objects. Do not open connections, register listeners, or start servers here.
  2. init() - Set up resources and register event handlers. Use this for database clients, event listeners, and dependencies that need the BSB context.
  3. run() - Start long-lived processing after every service has initialized: HTTP listeners, queue consumers, schedulers, and background loops.
  4. dispose() - Close what init() or run() opened.

Do not put everything in run(). If a service needs a generated client, create it in the constructor. If it needs to register event handlers or connect resources, do that in init(). Keep run() for actually starting the service.

Services can declare dependencies on other services using initBeforePlugins, initAfterPlugins, runBeforePlugins, and runAfterPlugins.

Type-Safe Events with Schemas

BSB uses schemas to define event contracts. This gives you autocomplete, type checking, and runtime validation:

// In init() - register listener. Trace comes FROM the caller.
async init(trace: Trace) {
  await this.events.onReturnableEvent('user.validate',
    async (callerTrace, data) => {
      // callerTrace lets you see who called this event
      return { valid: data.email.includes('@') };
    }
  );
}

// In run() - emit event. Pass YOUR trace so callers can track you.
async run(trace: Trace) {
  const result = await this.events.emitEventAndReturn(
    'user.validate',
    trace,  // pass startup trace for observability
    { email: 'user@example.com' },
    5  // timeout seconds
  );
}
// In Init() - register listener. Trace comes FROM the caller.
func (p *Plugin) Init(ctx context.Context) error {
    p.events.OnReturnableEvent("user.validate",
        func(ctx context.Context, data ValidateUserInput) (ValidateUserOutput, error) {
            // ctx contains the caller's trace
            return ValidateUserOutput{Valid: true}, nil
        },
    )
    return nil
}

// In Run() - emit event. Pass YOUR trace so callers can track you.
func (p *Plugin) Run(ctx context.Context) error {
    result, err := events.EmitAndReturn[ValidateUserOutput](
        ctx,  // pass startup context for observability
        "user.validate",
        ValidateUserInput{Email: "user@example.com"},
        5*time.Second,
    )
    return err
}
# In init() - register listener. Trace comes FROM the caller.
async def init(self, trace: Trace):
    @self.events.on_returnable_event("user.validate")
    async def validate(caller_trace: Trace, data: ValidateUserInput):
        # caller_trace lets you see who called this event
        return ValidateUserOutput(valid="@" in data.email)

# In run() - emit event. Pass YOUR trace so callers can track you.
async def run(self, trace: Trace):
    result = await self.events.emit_and_return(
        "user.validate",
        trace,  # pass startup trace for observability
        ValidateUserInput(email="user@example.com"),
        timeout=5
    )

Ready to Build?

Now that you understand the concepts, create your first service plugin.

Start with Node.js