Understanding BSB
BSB (Better Service Base) is an event-driven microservices framework with a pluggable architecture. Learn the core concepts before you start building.
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 Zod. 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:
Plugin Types
BSB has five types of plugins. Service plugins are where you write your business logic. The other four 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.)
- logging-* - Handles log output (console, Graylog, Datadog, etc.)
- metrics-* - Collects metrics (Prometheus, OpenTelemetry, 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:
- Constructor - Plugin is instantiated with configuration
- init() - Set up event listeners and dependencies
- run() - Start processing (called after all services init)
- dispose() - Clean up on shutdown
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
)