Back to projects
Aug 14, 2026
5 min read

Snow White

A serverless AWS pipeline for ingesting, processing and storing electricity meter data, named after the seven dwarfs.

Snow White

Work has me building and running a meter data platform, and this is my own version, built from scratch to explore the same problem: take payloads from electricity meters, process them, store them cheaply, and get them back out again fast. It’s serverless, it’s on AWS, and every subsystem is named after one of the seven dwarfs.

The shape of it

A payload lands, gets ingested, processed, enriched, stored hot, then eventually migrated to cold storage - and can be pulled back out at any point along the way.

System architecture

Dwarf Role AWS services
Sneezy Ingests payloads via REST API or SFTP API Gateway, Transfer Family, Lambda, Kinesis
Grumpy Processes Kinesis records - enriches and transforms data streams Lambda, Kinesis, DynamoDB
Bashful Meter configuration CRUD, Cognito-protected API Gateway, Lambda, DynamoDB
Happy Hot storage - fast read/write, 7-day TTL DynamoDB (PAY_PER_REQUEST, PITR, Streams)
Sleepy Cold storage - long-term archive (7 years) S3 Intelligent-Tiering
Dopey Retrieval - hot first, cold fallback with re-warm API Gateway, Lambda
Doc Nightly synthetic data generator for testing EventBridge Scheduler, SQS, Lambda
Snow White React/Vite UI for meters, payloads, ingest, retrieval S3, CloudFront, Cognito

A payload arrives at Sneezy, either as a REST call or dropped onto an SFTP site, and gets pushed straight onto a Kinesis stream with as little processing as possible - the goal is to keep ingestion thin and fast.

Grumpy picks it up from Kinesis, looks up the meter’s configuration in Bashful, and splits the payload into its individual data streams, each one enriched with “how to process it” instructions from that config. Processing is done with a chain of small, composable cartridges:

Cartridge Effect
passthrough No-op
scaling Multiply all values by a factor
unit-conversion Linear transform: value = value * factor + offset
quality-flag Mark out-of-range measurements as "suspect"
rule Apply conditional rules (set value or flag on lt/gt/eq conditions)

More than one cartridge can be chained onto a single data stream, which turned out to be a nice way of keeping processing logic simple and testable while still supporting fairly involved transformations.

Hot, then cold

Processed streams land in Happy - DynamoDB, tuned for speed, with a 7-day TTL. After that window, a stream migrates to Sleepy, cheap S3 storage meant to hold data for up to seven years.

Happy and Grumpy architecture

Getting data back out is Dopey’s job. It checks Happy first; on a miss, it falls back to Sleepy, and - this was the fiddly bit to get right - re-warms the data by writing it back into Happy for a couple of days, in case it’s needed again soon.

Happy architecture

A payload

{
  "meter": { "serialNumber": "MTR-001", "manufacturer": "Acme", "type": "X100" },
  "unitOfWork": { "unitOfWorkTimeStamp": "2026-06-19T00:00:00Z" },
  "dataStreams": [
    {
      "name": "active-energy-import",
      "period": 900,
      "details": {},
      "measurements": [
        { "timestamp": "2026-06-19T00:00:00Z", "value": 42.3, "details": {} }
      ]
    }
  ]
}

Scale

The design target is 10,000,000 payloads an hour - about 2,778 a second, sustained. With an average of 10 data streams per payload, that’s roughly 27,780 individual stream records a second flowing through Grumpy. Everything is Lambda and Kinesis rather than anything long-running, which makes that kind of throughput a matter of concurrency limits and shard counts rather than needing to provision servers for peak load.

Doc helps test that at scale too - it’s a nightly job that generates synthetic meter data and fires it at Sneezy the same way a real meter would, so the whole pipeline gets exercised even when there’s no real traffic.

Keeping it honest

There’s a UI - Snow White herself - a React/Vite app behind Cognito for browsing meters, payloads and retrieved data, which made debugging the rest of the system much less painful than reading DynamoDB items by hand.

I also leaned on Claude for a lot of this: working through the DLQ replay strategy in the runbook, generating the Terraform for each subsystem, and running a security review over the whole thing before I called it done. It’s the kind of project where getting the design right up front - what goes hot, what goes cold, how retrieval re-warms data - mattered more than any individual line of code, and having something to think it through with out loud was genuinely useful.

83 pytest tests cover the Python side; Terraform is split one module per subsystem plus shared modules for the Kinesis bus, SNS and the UI, with separate dev and prod environments.