Build series · Notifications

Notifications.

I studied how Uber, Duolingo, LinkedIn and Netflix build their notification services, then designed RepGarden’s from what they do. Here are the five rules the big apps share, the three steps every RepGarden notification goes through, and how I decide which notifications reach the lock screen.

By Ahmed Gadir8 min read

What the big apps do

A notification is the only time an app gets to speak first. Everything else in an app waits for someone to remember it exists.

RepGarden is the workout app I’m building for Benji, a personal trainer, and his clients. Benji sets his clients’ training on a website, and the clients log it on their phones, where a vegetable patch grows from the sets they do. None of the app is built yet, and the series page has the story so far.

Before designing RepGarden’s notifications, I studied how the companies that send the most notifications build theirs. Uber, LinkedIn, Duolingo, Netflix, Slack and Instagram have all written about it on their engineering blogs. The apps are very different, and between them their posts describe the same five rules.

  • Every notification is saved before it is sent. Uber saves each marketing push in an inbox for that user, then plans when to send it. [1] Duolingo stores each learner’s reminders in a database, and a separate process sends the reminders that are due. [2]
  • Urgent notifications and scheduled notifications travel on separate paths. LinkedIn sends messages between members on a fast path and its daily digest on a slow one. [3] Netflix sorts notifications by priority into separate queues, so a problem in one queue does not hold up the others. [4]
  • Each notification is checked again just before it is sent. LinkedIn drops a notification if the member has already acted on it, or if the notification has gone out of date. [3]
  • The list inside the app is the record, and the push is an extra. For each notification, LinkedIn decides between showing it in the app only, and showing it in the app and pushing it to the phone as well. [3]
  • The number of notifications is guarded. At Duolingo, the team could improve the timing and wording of notifications freely, but could not send more notifications without strong justification and the CEO’s approval. [5]

The apps that personal trainers use with their clients today are much simpler. Most of those apps send one reminder a day at a fixed time. Of the sixteen coaching platforms I looked at, none says in its help pages that it skips the reminder once the client has logged the workout.

Three steps for every notification

RepGarden follows the same five rules on a much smaller scale: one trainer and his clients.

Two things can start a RepGarden notification:

  • Something happens. For example, Benji replies to a set that a client filmed.
  • A scheduled time arrives. For example, a client asked to be reminded at 18:30 on training days, and it is now 18:30 on a day when that client has a session.

Whichever way a notification starts, it then goes through the same three steps.

Write it down, check it, send itTwo boxes on the left, something happens and a set time arrives, lead into three numbered steps. One, write it down: one row for each notification, and the same one can never exist twice. Two, check it: is the notification still true, and did the client turn this kind of notification off. Three, send it: through Expo, with an expiry. An arrow from step one leads down to the bell in the app, and an arrow from step three leads down to a push on the lock screen.Something happensthe trainer repliesA set time arrivesa reminder is due1Write it downOne row for each. The sameone can never exist twice.2Check itIs it still true? Did theclient turn this kind off?3Send itThrough Expo, with an expiry.The bellwaits in the appA pushon the lock screen
The bell in the app reads the rows saved in step one, so a notification appears behind the bell even when no push is sent.

Step one: write it down. Before RepGarden sends anything, the server saves a row in the database for the notification. When Benji saves a reply, the row for its notification is saved at the same moment, so a reply never exists without one. Each row has a key that can exist only once: this reply, or this session on this date.

The server could skip the row and just send the push. Saving the row first does three jobs that sending alone cannot do:

  • The row makes a double send impossible. If the timer runs twice, or the server tries a failed send again, the second attempt finds the row already there. A bug report from one open-source project describes what happens otherwise: the project sent the push first and marked the row afterwards, so a single slow send could start two runs at once and send two pushes. [6]
  • The row survives a crash. If the server stops halfway through sending, the row is still waiting when the server comes back.
  • The rows are the list behind the bell. The list of notifications that a client opens inside the app is read from the same rows.

Step two: check it. Just before sending, the server asks two questions about the notification. Is the notification still true? For a reminder, the session must still be planned and the client must not have started it. For a reply, the client must not have read it already. And has the client turned this kind of notification off in the app’s settings? If the notification is no longer true, or the client has turned that kind off, the server sends nothing and records the reason on the row. The first question is the rule I would put above every other: never notify someone about something they have already done.

Step three: send it. RepGarden’s phone app is built with Expo, a set of tools for building phone apps, and Expo runs a free service that passes each push on to Apple and Google. [7] Every push that RepGarden sends carries an expiry: two hours for a session reminder, three days for a reply. A push with no expiry can wait up to four weeks for a phone that is switched off, [7] so Monday’s reminder could arrive on Thursday.

The next diagram follows one reply from Benji through the three steps.

What happens when the trainer repliesSeven steps between the trainer’s web app, the server, the database, Expo and the client’s phone. One, the trainer saves a reply. Two, the server saves the reply and a notification row together. Three, it checks the row: is the reply still unread, and has the client left this kind of notification turned on. Four, it sends the push to Expo with an expiry. Five, Expo passes it to Apple or Google, who show it on the phone. Six, the server marks the row sent. Seven, when the client taps the push, the app opens the set and reports the tap to the server.1WEB APP → SERVERThe trainer saves a replyon the clip.2SERVER → DATABASESaves the reply and anotification row, together.3SERVER → DATABASEChecks it: is the reply unread?Is this kind still turned on?4SERVER → EXPOSends the push, to expirein three days.5EXPO → PHONEPasses it to Apple or Google,who show it.6SERVER → DATABASEMarks the row sent.7PHONE → SERVERA tap opens the set, andthe app reports it.
In step two the server saves the reply and its notification together, so a reply never exists without a notification.

A push, or the bell?

Not every notification is important enough for the lock screen. RepGarden puts each kind of notification in one of two places. A push appears on the client’s lock screen and interrupts the client. A bell item waits in a list inside the app, behind a bell icon, until the client next opens the app. A notification gets a push when it is about now. Either a person is waiting on the client, as with a reply from Benji, or it is time for the client to do something, as with a session reminder. A notification that the client can read at any time, such as a change to next week’s plan, goes behind the bell.

NotificationA pushBehind the bell
Your trainer replied on a filmed setYes, every replyYes
Your week is inYes, once for each weekYes
Your plan was updatedNoYes
Your trainer asked for a clipNoYes
Session reminderYesNo, the home screen already shows the session
Missed-session nudge, the morning afterYesNo, the home screen shows the missed day
The weekly harvest, on Sunday eveningYesYes

The bell matters more than it looks. In one benchmark of 10,000 apps, 56% of iPhone users had notifications switched on, [8] which leaves nearly half of iPhone users who never see a push. For those clients, the bell is how they hear from their trainer.

Plan changes go behind the bell because of a complaint I found on the ideas forum of another coaching product. On that forum, a request for the product to stop telling clients about every small change to a plan had 295 votes. [9] In RepGarden, if Benji edits a plan five times in an evening, the client gets five quiet lines in the app and nothing on the lock screen.

What Expo does, and what I build

The parts of the notification serviceThe trainer’s web app sends replies and plans to RepGarden’s server. The server holds a database with three tables, notifications, devices and settings, a timer that asks every minute who is due, and a sender that checks each row and then sends it. The sender hands each push to Expo, highlighted, which passes it to Apple or Google, who show it on the client’s phone. The phone also reads the bell’s list from the server.Trainer’s web appRepGarden’s serverThe databaseNotifications · Devices · SettingsThe timerEvery minute:who is due?The senderChecks each row,then sends it.Expo push serviceApple or GoogleClient’s phoneshows the push and the bell
RepGarden’s server is the part I build. Expo, highlighted, is the service RepGarden leans on to reach both iPhones and Android phones.

Expo asks the phone for permission, gives each phone an address that pushes are sent to, and delivers to iPhones and Android phones through one service. RepGarden’s server does everything else: it decides which client is told what and when, makes sure no notification is sent twice, and keeps the bell’s list and the switches in settings.

One detail in the server’s list of phones prevents a serious mistake. A phone keeps the same address whichever client signs in on it. If the server does not move the address when a second client signs in, the second client receives the first client’s notifications. RepGarden therefore ties each address to one account at a time.

More advanced features, left for later

The big apps do more than the three steps, and RepGarden’s first design did too. I cut these features from the first version, because none of them matters until the three steps work:

  • Quiet hours hold a late reply until the morning. Phones already have Do Not Disturb, and each client chooses their own reminder time.
  • A daily limit caps the automatic notifications. RepGarden’s automatic notifications rarely share a day anyway: reminders go out on training days, the missed-session nudge on free days, and the harvest on Sunday evening.
  • Bundling sends several replies that arrive within a few minutes as one push.
  • Easing off sends fewer reminders to a client who ignores them. In the first version, RepGarden always reminds a client unless that client switches reminders off.
  • A receipts check asks Expo, a few minutes after a send, whether Apple or Google accepted the push.
  • A measurement of whether reminders work holds back one reminder in ten at random and compares the results.

Each feature can be added later without changing the three steps. I don’t know yet which feature RepGarden will need first. My guess is the receipts check, because a reply that never arrives is the failure I care about most.

Where things stand

The design on this page was decided on 15 November. None of it is built. I have not yet chosen what RepGarden’s server will run on, and the design does not depend on that choice. I still need to ask Benji whether he wants an email when clips are waiting for him.

The next thing to design is a single screen. An iPhone shows its “allow notifications?” question once, ever. [10] The screen that RepGarden shows just before that question tells a client what RepGarden will notify them about and asks what time to remind them. That screen gets one chance.

Sources

  1. Uber Engineering — How Uber Optimizes the Timing of Push Notifications using ML and Linear Programming. November 2022. Every push is stored in a per-user inbox, the whole inbox is planned again when a new one arrives, and a last check runs at the moment of sending.

  2. Duolingo — Solving database contention with optimistic locking. December 2025. A post about a bug, which describes the reminder service along the way: each learner’s reminders are stored as a row, and a separate process sends the ones that are due.

  3. LinkedIn Engineering — Air Traffic Controller: Member-First Notifications at LinkedIn. March 2018. One service decides on every notification, and its stages are listed in order.

  4. Netflix Technology Blog — Rapid Event Notification System at Netflix. February 2022. Priority queues, a check that drops events that have gone stale, and a stored copy that devices fetch when a push doesn’t reach them.

  5. Jorge Mazal — How Duolingo reignited user growth. Lenny’s Newsletter, February 2023. A first-hand account by Duolingo’s former chief product officer, with the rule about notification volume.

  6. RAR1741/hub — issue 289. September 2026. A bug report from an open-source project whose reminder job read the unsent rows, sent them and then marked them sent, with nothing to stop two runs overlapping.

  7. Expo — Send notifications with the Expo Push Service. The message format, including the expiry and its four-week default, and what Expo’s responses do and don’t confirm.

  8. Batch — Push notification and mobile CRM benchmark 2025. Messages sent through 10,000 apps between July 2024 and July 2025. Batch sells notification software, so read it as a vendor’s figures: 56% of iPhone users and 67% of Android users opted in.

  9. ABC Trainerize ideas forum — a request not to inform clients of every simple change. Open since 2014, with 295 votes when I read it.

  10. Apple — Asking permission to use notifications. The first request shows the prompt and records the answer; later requests don’t prompt again.