Event-Driven Architectures
- Event
- The Innovation Lab
- Overview
An interactive exploration of event-driven architectures using Effect v4's PubSub primitives. Build your own pub/sub topology, watch events animate through the system in real-time, and see full OpenTelemetry traces — all backed by type-safe schemas, scoped resource management, and stream composition.
- Technologies
- Event-DrivenPub/SubEffectOpenTelemetryAstroArchitecture

Overview
This is a hands-on exploration of event-driven application architectures, focusing on the publish/subscribe (pub/sub) model built with Bun, Effect, and Astro. Rather than reading about event flow in the abstract, you can build your own topology, publish events, and watch them propagate visually in real time.
The interactive demo below lets you experiment with topics, subscribers, and fan-out patterns — all backed by a real Effect-powered backend with full OpenTelemetry observability. Want to skip the theory? Jump to the interactive demo and try it yourself.
Watch events animate through your topology in real time. See how one publish fans out to multiple subscribers across different topics.
Build your own pub/sub topology. Add topics, connect subscribers, and publish events to see the architecture in action.
Every publish and delivery is traced with OpenTelemetry. Effect.fn creates spans automatically, making event flows transparent end-to-end.
Defining Event-Driven Applications
Event-driven applications are systems where the core logic and state changes are triggered by events: discrete, immutable records representing occurrences within the application or its environment.
Loose Coupling
Unlike traditional architectures that rely on direct function calls or tightly coupled components, event-driven applications achieve loose coupling by having components emit events and react to them independently. Publishers don't know who is listening, and subscribers don't know who published.
Fan-Out in Action
In the demo below, you'll see this in action: when you publish an event to a topic, every subscriber on that topic receives it independently. Add multiple subscribers to a single topic and watch how one publish fans out across the topology in real time via Server-Sent Events.
Design Benefits
This design improves scalability and flexibility, allowing features to evolve independently as the system grows. Components don't need to know about each other's existence, only about the events they care about. The interactive topology builder demonstrates this — add or remove topics without affecting existing subscribers.
Key Terminology
Understanding these core concepts is essential for building robust event-driven systems
| Term | Definition |
|---|---|
| Event | An immutable, typed value representing a discrete occurrence in the system. Defined as an Effect Schema (e.g., Schema.Struct({ type, topic, user, content, timestamp })) for compile-time validation. |
| Topic | A named channel that groups related events. Publishers target a topic, and subscribers receive only events from topics they're interested in. Enables logical separation of event streams. |
| PubSub | Effect's bounded, backpressure-aware message bus (PubSub.bounded). Supports multiple publishers and subscribers with configurable capacity and optional replay buffers for late subscribers. |
| Stream | An asynchronous, pull-based sequence of events created via Stream.fromPubSub(). Supports compositional transformations (filter, map, merge) with automatic backpressure. |
| Subscription | A scoped connection to a PubSub channel. Created via PubSub.subscribe() with automatic cleanup — when the scope closes, the subscription is released and resources freed. |
| Scope | Effect's resource lifecycle primitive. Ensures subscriptions, connections, and other resources are cleaned up deterministically via Effect.addFinalizer(), preventing leaks even on errors. |
| Span | An OpenTelemetry trace segment created automatically by Effect.fn(). Annotated with event metadata (topic.name, event.type) to make event flows observable end-to-end. |
Publish/Subscribe: Pattern Overview
The publish/subscribe (pub/sub) pattern enables components to communicate indirectly by publishing events to named topics, with interested parties subscribing to receive only the events they care about. Multiple topics allow logical separation of event streams, so publishers and subscribers remain fully decoupled while supporting scalable, reactive architectures.
Effect's PubSub primitive provides a bounded, backpressure-aware message bus. Stream.fromPubSub converts subscriptions into composable, pull-based streams, while Effect.fn automatically creates OpenTelemetry spans for every publish and delivery — making event flows observable end-to-end with zero boilerplate.
Multi-Topic Fan-Out
Components emit events to specific topics without knowing consumers
Topics
React to events from subscribed topics as composable streams
Schema-validated events with compile-time inference
Every publish and delivery traced with OpenTelemetry spans
Scoped subscriptions with Effect.addFinalizer cleanup
Benefits and Tradeoffs in Event-Driven Systems
Benefits
Loose Coupling
Publishers and subscribers never reference each other directly. New consumers can be added without modifying existing producers, enabling independent team ownership and deployment.
Full Observability
Every event publish, fan-out, and delivery is automatically traced via Effect.fn spans and OpenTelemetry. Distributed traces tell the full story from publish through fan-out to each subscriber's receipt.
Scalable by Design
Topics provide natural partitioning. Each topic manages its own bounded channel with independent backpressure, allowing the system to scale by adding topics rather than reconfiguring a monolithic bus.
Declarative Dataflow
Event pipelines are composed with Stream operators — filter, map, merge, take. Processing logic is explicit, testable, and benefits from Effect's type inference.
Tradeoffs
Eventual Consistency
Events are asynchronous by nature. Subscribers may see state changes at different times, and there's no built-in guarantee of ordering across topics. Designs must tolerate temporary inconsistency.
Delivery Semantics
In-memory PubSub provides at-most-once delivery. If a subscriber disconnects, missed events are lost. Production systems may need replay buffers, persistent event logs, or acknowledgment protocols.
Debugging Fan-out
When one publish fans out to N subscribers across M topics, tracing the full path requires structured logging and correlation IDs. Without proper instrumentation, the causal chain becomes opaque.
Key Insight: Event-driven architectures paired with Effect's type system and OpenTelemetry integration turn traditional observability challenges into strengths — every event flow is automatically instrumented, making the system's behavior transparent by default.
PubSub Control Plane
Connect to the event stream, then subscribe to topics to receive their messages. Only subscribed topics appear in your event feed. Click topic nodes in the topology or use the subscribe buttons to toggle. Publish messages from the bar below and watch them fan out to all subscribers.
Add a topic and connect to see the event flow
Subscribe to a topic, then publish a message
Connect to view metrics
Architecture Diagram
Client Layer
EventSource APIReact components with EventSource (SSE) and tracedFetch
Transport
HTTP / SSESSE for real-time events, HTTP for commands (REST API)
API Handlers
Effect.fnEffect.fn handlers that delegate to PubSubService
PubSubService
PubSub.boundedMulti-topic PubSub with per-topic channels and global stream
Observability
OpenTelemetryOpenTelemetry spans flow through every layer
Data Flow Steps
Connect
Client establishes SSE connection to /pubsub/stream
Subscribe
Server creates subscription to global PubSub stream
Publish
Client publishes events via POST /pubsub/publish with traceparent header
Distribute
PubSubService delivers to topic-specific and global subscribers
Stream
SSE streams events back to all connected clients
Cleanup
On disconnect, scoped subscription is automatically released
Implementation Details
Events are defined with Effect Schema for runtime validation and type safety across the full stack:
export const PubSubEventType = Schema.Literals([
"message",
"system",
"join",
"leave",
]);
export const PubSubEvent = Schema.Struct({
id: Schema.String,
topic: Schema.String,
type: PubSubEventType,
user: Schema.String,
content: Schema.optional(Schema.String),
timestamp: Schema.Number,
});
export type PubSubEvent = typeof PubSubEvent.Type;Scaling Patterns
The patterns demonstrated in the interactive demo above are designed to scale from prototype to production
Each topic runs its own bounded PubSub channel. One slow consumer cannot block events flowing through other topics.
const topicPubSub = yield* PubSub.bounded<PubSubEvent>(100)
topics.set(name, {
pubsub: topicPubSub,
subscriberCount: 0,
messagesPublished: 0
})Multiple streams merge cleanly. Event streams, heartbeats, and filters compose without coupling.
const combined = Stream.mergeAll(
[formattedEvents, heartbeatStream],
{ concurrency: 2 }
)Subscriptions clean up automatically when scope closes. No manual unsubscribe, no dangling connections.
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
subscriberCount--
yield* Effect.log("Subscriber disconnected")
})
)Capacity-limited channels prevent memory exhaustion under load
Add topics without affecting throughput of existing ones
Scoped resources guarantee cleanup on disconnect
Single Process
In-memory PubSub per topic — what the demo above uses. Ideal for development and single-server deployments.
Persistent Events
Add an Event Log for replay and durability. Subscribers can catch up after reconnecting.
Distributed
Effect Cluster for cross-process pub/sub. Same PubSub/Stream primitives, distributed runtime.
Conclusion
The interactive demo above puts these ideas into practice. You built a topology with the visual builder, watched events flow between topics in real time, and saw metrics update as subscribers processed messages. Every operation was traced with OpenTelemetry spans, and every subscription cleaned itself up automatically when its scope closed.
This is what Effect makes possible: event-driven systems where type safety, resource management, and observability are structural guarantees — not afterthoughts bolted on later.
The demo demonstrated a complete event-driven system running in the browser: a visual topology builder for defining topics and subscriptions, animated event flow showing messages propagating through the graph, and real-time metrics tracking throughput per topic — all backed by the same Effect primitives used in production systems.
Schema-validated events with compile-time inference across the full stack
Scoped subscriptions with automatic cleanup via Effect.addFinalizer
Every operation traced with OpenTelemetry spans via Effect.fn
Ready to transform your engineering?
Whether you need technical leadership, enterprise development, or team optimization—let's discuss how we can help.