Field notes · Microservices

Seven O'Clock, Every Morning

or: building a daily scripture service, from docker compose to AWS

Prefer to watch? Every animation in this post, narrated in about four and a half minutes, with captions built in. The narration is a local AI clone of my voice.

Daily Bible sends every subscriber the day's reading, a saint's quote, an AI image and a video, in English, Hindi, Marathi or Spanish, on WhatsApp or email, at 7 AM their time. Here's how I built it as microservices, tested it all on my laptop, then moved it to AWS. Everything you need to rebuild it is in the diagrams.

What this is

My own project, designed and built end to end. Latency, throughput and cost figures come from local load tests or are illustrative estimates, and are labelled that way.

The short version, for whichever hat you're wearing

  • Make the day's content once, send it many times. Generation is a fixed daily cost; delivery is the only part that grows.
  • Every step can fail and the morning still happens: retries, fallbacks, and a ladder that ends in plain text.
  • Built and tested on a laptop first, then each piece swapped for its AWS twin.
01

What lands at 7 AM

Interactive · one morning, four languages, two channels
01·b

The screens

Mockups · click through the product
02scale

Eleven small services

Each does one job, owns its data, and talks through queues. Press play to follow one morning.

Animation · from the 18:00 UTC alarm to a message in your hand
0 / 13Press play to follow one morning through the system.
The Daily Bible services. A scheduler starts an orchestrator, which calls the reading, writer, translation, image, audio and video services in parallel, stores one package per date, and hands it to a fan-out service. Fan-out reads subscribers from the subscriber service and puts one message per subscriber on the email or WhatsApp queue, released at 7 AM local time. Email and WhatsApp senders deliver, and every service reports to observability. scheduler · 18:00 UTC, the day before orchestratora state machine · retries · checkpoints readingcalendar + text writerquote + reflection translatehi · mr · es imageone painting audio4 voices videofind or render quality gate · per language package storeone package per date · object storage + key-value subscribersAPI + Postgres fan-outreleased per zone at 7 AM email queueone message per subscriber WhatsApp queueone message per subscriber email senderpaced · outbox WhatsApp senderpaced · breaker dead letters · alarm · one-click redrive a phone at 7:00 AM observabilitytraces · metrics · on-time rate · alarms
    03speed

    Laptop first, then AWS

    Every service talks to an interface, not a vendor. Locally, a container plays each AWS part. Flip it:

    Interactive · the same contracts, two homes

    docker-compose.yml · the local stack (trimmed)
    services:
      postgres:   { image: postgres:16 }
      localstack: { image: localstack/localstack, environment: { SERVICES: sqs,sns,dynamodb,stepfunctions,events } }
      minio:      { image: minio/minio, command: server /data }
      mailpit:    { image: axllent/mailpit }          # every email lands here, viewable at :8025
      wa-mock:    { image: wiremock/wiremock }        # fake WhatsApp Cloud API: records, can return 429s
      reading:    { build: ./services/reading }
      writer:     { build: ./services/writer, environment: { LLM_MODE: fixtures } }
      # ... translate, image, audio, video, fanout, sender-email, sender-whatsapp, subscribers
    
    04speed

    Make it once, in parallel

    Interactive · the daily pipeline, two schedules
    A timeline of the pipeline stages.

    Pick a schedule.

    seconds, from local test runs with recorded model responses replaced by real calls · illustrative

    05reliability

    Run it twice, get one morning

    Animation · crashes and double triggers
    1. Pick a failure.
    06scale

    Seven o'clock, everywhere

    One package, sent in waves as 7 AM sweeps around the world. A schedule per time zone fires at 7 AM local and fans out only that zone's subscribers. (SQS can only delay a message 15 minutes, so the queue can't do the waiting.)

    Animation · one day of sends, from 18:00 UTC
    Send windows for each time zone across one UTC day.

    Press play.

    07speedfailover

    Pace, don't burst

    WhatsApp and email providers cap messages per second. Blast them and you get throttled; pace them and nothing fails.

    Interactive · 60,000 Indian subscribers at 7:00 IST
    Messages sent per second against the provider's limit.

    Pick a sender.

    illustrative: a limit of 80 messages a second

    08failover

    A video, no matter what

    Animation · the video ladder
    1. a homily for today's readingsearch trusted Catholic channels, in the subscriber's language, 3–20 minutes
    2. a reading videosomeone reading today's passage, same checks
    3. render our ownthe day's image + narration + captions, made in ~2 minutes
    4. image + audio onlythe voice note still goes out
    5. text onlythe reading and the quote, always

    Pick a day.

    09reliability

    Never twice, never lost

    Animation · one message through the sender
    1. Pick a failure.
    10reliabilityvisibility

    Checks before anything sends

    The rule that matters most: models never write scripture. The text comes from a stored, versioned Bible; models only write around it.

    Animation · the quality gate, per language

    Pick a day.

    11visibility

    One number: on time

    Interactive · the morning dashboard, and a WhatsApp outage
    All channels healthy.

    simulated: one tick per minute during the India morning wave

    12scale

    Where the money goes

    Interactive · monthly cost as subscribers grow

    illustrative unit costs; half the subscribers on WhatsApp, half on email

    13

    Rebuild it yourself

    1. Load the Bible once. Public-domain text per language into a verses table; a liturgical calendar maps each date to a reading.
    2. Write each service against an interface: Queue, Blob, KV, Mailer, WhatsApp, LLM. One local and one AWS implementation each.
    3. Run the stack with compose, models in fixture mode, then real calls. Mailpit shows every email; the WhatsApp mock can return 429s on demand.
    4. Test the failures, not just the path: kill containers mid-run, fire the scheduler twice, make the mock throttle.
    5. Port one piece at a time: containers to ECS Fargate, LocalStack to SQS/SNS/DynamoDB, MinIO to S3, Postgres to Aurora, Mailpit to SES, the mock to the WhatsApp Cloud API, all in CDK.
    6. Canary first: one time zone and one language for a week, watching the on-time rate, then everyone.
    the repository
    daily-bible/
      services/   reading  writer  translate  image  audio  video
                  fanout  sender-email  sender-whatsapp  subscribers  orchestrator
      libs/       ports/ (Queue, Blob, KV, Mailer, WhatsApp, LLM)  adapters/local  adapters/aws
      infra/      docker-compose.yml  cdk/ (VPC, ECS, SQS, DynamoDB, Aurora, SES, Step Functions)
      data/       bible/.sqlite  calendar/  quotes.yaml (saints, sourced)
      tests/      contract/  chaos/  load/
    the one message every sender reads
    { "key": "sub_8f2c:2026-11-05:whatsapp",
      "package": "s3://daily/2026-11-05/",
      "lang": "mr", "channel": "whatsapp",
      "zone": "Asia/Kolkata" }
    Sorry, quick interruption

    Machine translation has opinions. Ask for "the Good Shepherd" in Marathi with no context, and you can get a shepherd who's simply good at his job. That's why every language has a native-speaker gate.

    14

    Decisions, one page

    1. One package or one per subscriber?
      per subscriberone per date

      AI cost stays flat. Cost: no personalisation yet.

    2. Monolith or services?
      one appsmall services + queues

      A slow renderer can't stall sending. Cost: more to deploy.

    3. Where does scripture come from?
      the modela stored, versioned text

      Never misquote the Bible. Cost: one-time loading.

    4. How do sends get timed?
      delayed queue messagesa release schedule per time zone

      Queues only delay 15 minutes; schedules can wait hours. Cost: ~40 schedules to manage.

    5. How do retries stay safe?
      hopededupe key + idempotency key

      Crashes never double-send. Cost: a write per message.

    6. What if there's no video?
      skip itthe ladder

      Something always arrives. Cost: a renderer to run.

    7. How is it tested?
      in the cloudlocally, failures included

      Fast loops, no bill. Cost: keeping mocks honest.

    15

    The well-earned drink

    Every Brew Miles trip ends with one:

    1. Make it once, send it many times.
    2. Claim the date, checkpoint every stage, and reruns become free.
    3. Key every message, pace every channel, and keep a breaker per provider.
    4. Build a ladder whose last rung is plain text.
    5. Test it on a laptop, failures and all, then port one piece at a time.
    Further reading
    Show the docs this design leans on