/projects/dietbox-notifications
← All projectsDietbox Notifications
Everything the product sends out, moved into a service of its own.
Head of Technology · 2023–2024
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 service of its own that took over sending — and, because it owned every send, could account for them.
What I did
I wrote the design document, the domain and the service: 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, and the record of every notification sent that sits at the middle of it.
- The two controllers and their commands and queries — sending a notification, and reading back what was sent.
- 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 nothing in the product could say what it was spending it on — sends went out from several places and were recorded in none. The obvious place to fix that was the main product itself, but the main product was already too complex to extend safely. The alternative was to move sending out entirely: one service that owns the channel, records every message, and can be changed without risking the product it serves.
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.
Architecture
A calling service reaches the notify endpoint, which hands the message to the WhatsApp provider and records the result either way — so every message the product sends leaves a row behind it.
- Calling serviceAnother service in the platform requests a notification on a practitioner’s behalf.
- Notify endpointThe notify controller receives the request and dispatches the send command.
- Send commandThe send is handled as a named command, so the request and the record of it are the same story.
- ProviderThe WhatsApp integration sends the message through the official API, behind the crosscutting provider package.
- Sent recordThe outcome — sent or refused — is written to the record every notification leaves behind.
A notification, from request to record
- RequestedA calling service asks for a notification to be sent to a practitioner.
- AcceptedThe request is validated and turned into a send command against that practitioner.
- DispatchedThe message goes to the WhatsApp provider, behind the crosscutting package that wraps it.
- RecordedEither outcome is written to the record of notifications sent, so "did this message actually go out" has an answer that does not depend on asking the provider.
What it does
- A notify controller and the command that sends a practitioner’s notification.
- A nutritionist controller and queries over that practitioner’s history of sent notifications.
- A record of every notification sent, which is what makes the spend answerable after the fact.
- 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.
The metering was built and never switched on
A per-practitioner limit and a log of who changed it are in the domain, built so the cost could eventually be charged back to whoever generated it. That part was never put to use — what the service did every day was send and record. The decision stays on the card because the limit is still in the code, and leaving it out would describe a service that was never built.
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 sent record needing to change.