Back to Portfolio
ArticleTechnologyAug 19, 2025

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
Event-Driven Architectures preview
Introduction

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.

Visual Event Flow

Watch events animate through your topology in real time. See how one publish fans out to multiple subscribers across different topics.

Interactive Topology

Build your own pub/sub topology. Add topics, connect subscribers, and publish events to see the architecture in action.

Full Observability

Every publish and delivery is traced with OpenTelemetry. Effect.fn creates spans automatically, making event flows transparent end-to-end.

Core Concepts

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.

Glossary

Key Terminology

Understanding these core concepts is essential for building robust event-driven systems

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.
Pattern

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 Type-Safe Abstractions

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

Publishers

Components emit events to specific topics without knowing consumers

orders
notifications
analytics

Topics

Subscribers

React to events from subscribed topics as composable streams

Type-Safe

Schema-validated events with compile-time inference

Observable

Every publish and delivery traced with OpenTelemetry spans

Resource-Safe

Scoped subscriptions with Effect.addFinalizer cleanup

Considerations

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.

Live Demo

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.

Offline
Topics

Add a topic and connect to see the event flow

Event Stream

Subscribe to a topic, then publish a message

Metrics

Connect to view metrics

System Design

Architecture Diagram

Client Layer

EventSource API

React components with EventSource (SSE) and tracedFetch

Transport

HTTP / SSE

SSE for real-time events, HTTP for commands (REST API)

API Handlers

Effect.fn

Effect.fn handlers that delegate to PubSubService

PubSubService

PubSub.bounded

Multi-topic PubSub with per-topic channels and global stream

Observability

OpenTelemetry

OpenTelemetry spans flow through every layer

Data Flow Steps

1

Connect

Client establishes SSE connection to /pubsub/stream

2

Subscribe

Server creates subscription to global PubSub stream

3

Publish

Client publishes events via POST /pubsub/publish with traceparent header

4

Distribute

PubSubService delivers to topic-specific and global subscribers

5

Stream

SSE streams events back to all connected clients

6

Cleanup

On disconnect, scoped subscription is automatically released

Code Walkthrough

Implementation Details

Event Schema

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;
Effect SchemaRuntime ValidationShared Types
Architectural Properties

Scaling Patterns

The patterns demonstrated in the interactive demo above are designed to scale from prototype to production

Topic Isolation

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
})
Stream Composition

Multiple streams merge cleanly. Event streams, heartbeats, and filters compose without coupling.

const combined = Stream.mergeAll(
  [formattedEvents, heartbeatStream],
  { concurrency: 2 }
)
Scoped Resources

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")
  })
)
Bounded

Capacity-limited channels prevent memory exhaustion under load

Independent

Add topics without affecting throughput of existing ones

Zero-Leak

Scoped resources guarantee cleanup on disconnect

Scaling Progression
Phase 1

Single Process

In-memory PubSub per topic — what the demo above uses. Ideal for development and single-server deployments.

Phase 2

Persistent Events

Add an Event Log for replay and durability. Subscribers can catch up after reconnecting.

Phase 3

Distributed

Effect Cluster for cross-process pub/sub. Same PubSub/Stream primitives, distributed runtime.

Summary

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.

Effect Primitives Used
PubSub — bounded, per-topic message channels
Stream — composable async event sequences
Scope — automatic resource cleanup via finalizers
Schema — runtime-validated event types with compile-time inference
Effect.fn — automatic OpenTelemetry spans per operation
What You Saw

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.

Type-Safe

Schema-validated events with compile-time inference across the full stack

Zero Leaks

Scoped subscriptions with automatic cleanup via Effect.addFinalizer

Observable

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.