Case study /10 · In production
AEMS

Context
Event data powers products like CLZTR, but no single source is complete or reliable. Venues post to their own sites, aggregators overlap, formats drift weekly. The only way to a trustworthy catalog is a pipeline that assumes every input is partially wrong.
Problem
Four failure modes had to be engineered away: missed events (coverage), broken records (validation), the same event appearing five times (deduplication), and events floating without usable coordinates (enrichment).
Constraints
- Sources change markup and structure without notice; the pipeline had to degrade per-source, not globally.
- Politeness and correctness toward sources: scheduling, rate limits and caching were mandatory.
- Deduplication had to work across sources that disagree on titles, times and venue naming.
- Downstream consumers needed a stable schema regardless of upstream chaos.
Responsibilities
- Crawler framework in Go with per-source adapters; Python extractors for irregular sources.
- Scheduling system controlling crawl cadence per source reliability and change rate.
- Validation and normalization stages enforcing the canonical event schema.
- Deduplication engine matching events across sources.
- Geographic enrichment resolving venues and addresses to coordinates.
- Publishing layer emitting structured events to PostgreSQL and Redis consumers.
Architecture
- A staged pipeline — acquire → extract → validate → normalize → deduplicate → enrich → publish — with every stage independently observable and retryable.
- Go handles concurrent crawling and orchestration; Python handles messy extraction where its parsing ecosystem wins.
- PostgreSQL stores canonical events and full stage lineage; Redis carries work queues and hot lookups.
- Failed records park in inspectable dead-letter queues per stage instead of vanishing.
Key decisions
- Schema-first: a strict canonical event model with explicit optionality, so downstream code never guesses.
- Deduplication by composite similarity — time proximity, venue resolution, fuzzy title matching — tuned to prefer false splits over false merges.
- Store source lineage on every record: when an event is wrong, you can see exactly which source and stage produced it.
- Per-source circuit breakers so one broken adapter can't poison a crawl cycle.
Implementation
- Adapters declare their capabilities and cadence; the scheduler composes crawl plans from those declarations.
- Validation produces typed rejection reasons, which feed a per-source quality score that in turn adjusts scheduling.
- Geographic enrichment resolves venue strings through a cached geocoding layer with manual-override support.
- Publishing is idempotent — replaying any stage is always safe.
Performance
- Concurrent crawling is bounded per source and globally; backpressure propagates through the queue layer.
- Deduplication candidates are pre-filtered by time and geographic buckets, keeping comparisons near-linear.
- Redis-backed caches keep repeat geocoding and repeat-fetch costs near zero.
Outcomes
- A continuously updated catalog of structured, deduplicated, geolocated events in production.
- Source breakage became a routine, per-adapter maintenance task instead of a system outage.
- The same pipeline architecture absorbed new sources without structural change.
- [PLACEHOLDER — add source counts and event volumes here when publishable.]
Lessons
- Pipelines are products: their operators are users, and observability is their interface.
- Designing for partial failure per source is what makes 'scalable' actually true.
- Data quality is an accumulation of small, boring, testable rules — not one clever algorithm.
Software Architecture

