The Observer Design Pattern Handbook: Event-Driven Architecture & Domain-Driven Design in Dart

2026-07-18 · source · source ↗ · raw capture · wiki page

A hands-on tutorial building the Observer pattern from first principles in Dart, then connecting it to Event-Driven Architecture (EDA), Domain-Driven Design (DDD), and Riverpod in Flutter.

The article starts with a concrete problem: a login function that saves tokens, caches user data, navigates, and tracks analytics — all in one method. Every new requirement forces modification of that function, risking breakage. The Observer pattern solves this by decoupling the event source from its consumers: the login logic does one thing and announces the result; independent observer classes each handle one side effect.

The Dart implementation walks through Observer and Subject interfaces, a concrete Subject that uses snapshot iteration (to prevent concurrent modification errors during notification) and per-observer try/catch (so one failing observer doesn't stop the chain). Concrete observers — Token, User, Navigation, Analytics — each have exactly one responsibility. A generic EventBus<T> then makes the pattern reusable across features without duplicating infrastructure.

The article notes that Flutter developers already use Observer constantly: StreamController is a Subject, ChangeNotifier.notifyListeners() iterates observers, and BLoC follows the same pattern.

On EDA: events are modeled as immutable facts (not commands), with DomainEvent carrying a timestamp and ID. A type-safe DomainEventBus uses Map<Type, List<EventHandler>> so only handlers registered for a specific event type fire.

On DDD and Clean Architecture: the domain layer is pure Dart with zero framework dependencies. Use cases orchestrate — they call repositories, raise domain events, return results — but never handle side effects directly. The Riverpod integration enforces a clean rule: handlers own side effects, the notifier owns UI state, and widgets own nothing. An AsyncNotifier reflects only use-case outcomes as AsyncLoading / AsyncData / AsyncError; all side effects run through event handlers independently of widget lifecycle.

Testing becomes trivial: use-case tests verify the right event type was published; handler tests verify one side effect in isolation; notifier tests verify state transitions only. Adding a new side effect means creating one handler class and adding one bus.register line — nothing else changes.

The article closes with guidance on when to use the pattern (one event → many independent reactions, growing side-effect counts) and when to skip it (single consumer, simple direct relationships, or when existing Flutter reactivity already suffices).