The useful starting point
Keep the business event, message delivery and user-visible state distinct. A notification should help someone understand the product, not become its only record of an important event.
Name the event before choosing a channel
Start with what changed in the product and who needs to know. A completed payment, a ride update and a request needing attention have different purposes. The event should be understandable before it is turned into a push notification, email or another message.
Then define the information and action the message should provide. This keeps the channel implementation connected to the task instead of producing a collection of disconnected alerts.
Trace the product event and the message separately
Illustrative example · a simplified payment-notification example, not a client incident
| Observation | Product record | Notification record | What a user or operator should be able to establish |
|---|---|---|---|
| A payment is confirmed | The agreed completion state is recorded | A message is requested | The underlying transaction exists independently of delivery |
| A provider accepts the message request | Payment state is unchanged | Accepted by the messaging provider | Acceptance is evidence about dispatch, not proof of reading |
| Delivery fails or remains unresolved | Payment state is unchanged | Failure or uncertainty is visible | The team can investigate the attempt and the app can still show the transaction |
| A retry is attempted | The payment is not repeated | Another delivery attempt is related to the same event | Communication retry and business action remain distinct |
| The user opens the app | The current transaction is available | Any observed message interaction is recorded separately | The product provides an authoritative route to the information |
The table is useful because it exposes a boundary: retrying communication must not inadvertently repeat the underlying action. It also makes clear which state a person is actually looking at when investigating a complaint.
Agree what the delivery labels mean
- 1Message requested
- 2Dispatch accepted or rejected
- 3Delivery information received if available
- 4User interaction observed if the product records it
A delivery label should describe an observation the system can support. “Sent”, “delivered” and “read” are not interchangeable. The exact reporting available depends on the channel and provider.
Keep the event reference, message identity and attempt history connected. Those references help someone explain whether a problem concerns the business event, the request to communicate it or a later delivery stage.
Decide whether a late message still helps
A status update can become misleading after the product has moved on. Define which messages remain useful, what should expire and where the person should check the current state.
A message can also be important enough to need another route to the information. That is a product decision to make explicitly. The shared notification service can support communication responsibilities, while the application remains responsible for explaining the current product state.
Centralise the responsibilities that are shared
When several services need to send messages, shared handling can give templates, routing, delivery attempts and logs an explicit home. That is one part of Codek’s payments engineering work: a dedicated notification service rather than separate dispatch implementations across the product.
A shared service still needs a clear boundary. The originating service should know what event occurred; the notification layer needs the information required to communicate it. Avoid making the template itself the only definition of the business event.
Do not confuse accepted with seen
Record delivery stages with names that reflect what is actually known. Sending a request to a provider, having it accepted and having a message reach a device are different observations. Evidence that a person read a message may be different again.
Firebase’s message-delivery documentation (opens in a new tab) illustrates why delivery reporting needs careful interpretation. The application should not promise more than its evidence supports.
Make retries useful rather than confusing
Decide when a message should be retried, when it is no longer relevant and how repeated requests are identified. An old status message arriving after a newer one can confuse a person even if both messages were technically delivered.
- Which product event triggered the message?
- Is repeated delivery handled intentionally?
- Can a late message become misleading?
- Where can the person check the current state?
- Can the team trace a failed delivery attempt?
Shared dispatch in our payments work
Our payments work includes a shared notification-dispatch service with templating, channel routing, retry handling and logging. Our mobility work also includes a separate notification service.
Giving dispatch an explicit responsibility makes it easier to discuss where message changes and delivery investigation belong. The event still originates in the product: a shared template or delivery attempt should not become the only record of what happened.
The payments case gives the broader context. The worked timeline in this guide explains the boundary between business state and communication; it does not reproduce a client event or promise a particular delivery rate.
Common questions
Does provider acceptance prove that a person saw a notification?
No. It establishes a particular stage in dispatch. The channel’s delivery information and any observation of user interaction need to be interpreted separately.
Should we retry every failed notification?
Define relevance, expiry and repeat handling for the event and channel. A late or duplicated message can confuse a user, so a retry policy needs to consider the current product state.