/projects/dietbox-notifications

All projects

Dietbox Notifications

A messaging bill turned into a product constraint.

Head of Technology · 2023–2024

.NET 6C#CQRSSQL ServerWhatsApp Business APIAzure DevOps
WebsitePrivate

Overview

This service exists because of a number on an invoice: the official WhatsApp messaging bill in May 2023. The answer was not a rate limit bolted onto the existing product, but a small domain of its own — a quota, a log of who changed it, and a record of every send.

What I did

The design document, the domain and the service are the author’s: nineteen of the twenty commits, from the first estimate to the running service.

  • The capacity-planning document itself — the volume, query-rate and storage estimates the service was built to meet.
  • The domain model: a notification limit per practitioner, a log of every change to it, and a record of every notification sent.
  • The two controllers and their commands and queries — adding a limit, sending a notification, and querying both limits and sent records.
  • The crosscutting packages behind the layers: the WhatsApp provider integration and dependency injection.

The problem

The official WhatsApp Business API bill arrived in May 2023, and the product had no way to meter what it was spending on it. The obvious place to add a limit was the main product itself — but the main product was already too complex to extend safely, and a cost control that risks the product it is protecting is not a cost control. The alternative was a service with zero impact on the product, able to serve other notification channels later.

By the numbers

These four figures come from the service’s own design document, written before a line of it existed — a capacity plan, not a production measurement taken afterward.

~51kmessages a monththe volume being paid for
~30kqueries a day0.3 QPS average
5peak QPS planned for
~1.4 GBstorage over ten years214 bytes per notification

Architecture

A calling service reaches the notify endpoint, which checks the practitioner’s quota before anything is sent, hands the message to the provider, and records the result either way.

  1. Calling serviceAnother service in the platform requests a notification on a practitioner’s behalf.
  2. Notify endpointThe notify controller receives the request and dispatches the send command.
  3. Quota checkThe practitioner’s limit is read before the send proceeds — no quota, no message.
  4. ProviderThe WhatsApp integration sends the message through the official API, behind the crosscutting provider package.
  5. Sent recordThe outcome — sent or refused — is written to the record every notification leaves behind.

A notification, from request to record

  1. RequestedA calling service asks for a notification to be sent to a practitioner.
  2. Quota checkedThe practitioner’s remaining limit is read against the request.
  3. Dispatched or refusedWithin quota, the message goes to the WhatsApp provider; over quota, the send is refused before it costs anything.
  4. RecordedEither outcome is written to the log of notifications sent, so the answer to "why was this blocked" already exists.

What it does

  • A notify controller and commands to send a notification and to add a practitioner’s limit.
  • A nutritionist controller and queries over that practitioner’s current limit and history of sent notifications.
  • Three domain models: the notification limit itself, a log of every change to it, and a record of every notification sent.
  • A layered service with crosscutting packages for the WhatsApp provider and dependency injection, kept separate from the domain they support.

Engineering decisions

  • A separate service specifically to be ignorable

    The stated goal was zero impact on the main product. Isolating the notification service meant it could be switched off, redeployed or rewritten without taking the product down with it — the opposite of bolting a limiter onto code that was already too complex to touch safely.

  • A quota is a domain model, not a rate limit

    A bare counter would have answered "can this send happen." Instead, the limit, a log of every change to it, and a record of every send together answer a harder question: why was this one blocked, and who changed the limit that blocked it.

  • Capacity planned before the first line

    The monthly volume, the query rate and the ten-year storage footprint were estimated in the design document before the service was built, which is why the storage decision — how much space this would ever need — was a boring, already-answered question rather than a surprise.

  • One provider first, the interface for more

    WhatsApp was the bill that started this, so it is the only provider that sends today — but email, SMS and push were the shape the domain and the API were designed to accept later, without the quota model or the sent record needing to change.