Notification Engine

Revant
Math Camp Engineering
3 min readJul 14, 2016

We built a system to aggregate and send notifications with a time component. This makes is super easy to send notifications like “Alice and 5 others liked 5 of your photos” and do things like only send one notification every 24 hours and combine all like notifs for 5 minutes before sending. Sending the right amount of notifications to a user at the right times helps us deliver higher engagement without annoying them.

The Problem

Notifications are useful and can be a key driver for engagement, but they can also get annoying really fast. Most products either send every notification (eg. Instagram) or chose to skip entire categories of notifications. Turns out its a lot of work to smartly deal with aggregation of notifications. It leads to a lot of complexity and is hard to change. Our product Shorts is a high frequency sharing app so we wanted to be able to aggregate notifications but still send them as soon as possible (for realtime feedback) and we wanted the ability to test multiple variations of it. Thus we designed a system to handle it.

Design

We achieved simplicity by decoupling the time and aggregation aspects of notifications. For developers this boils down to firing events in the app whenever some action happens eg. when user A likes user B’s (receiver) photo, the application fires a notif event of type like to the notification engine. They can then define the aggregation for like notifs separately.

In the application code it looks like:

notifengine.NewEngineEvent(ctx, notifengine.EventAtom {
For: "receiver",
EventType: "like",
From: "me",
})

For the aggregation part, each notification type has to define 2 methods:

// This method takes in the previous state and the new event and outputs the time to schedule this type of notificaton
eta(State, Event) -> time
// This method takes in a list of events and creates a push notification with text combining all of those events. eg. "Alice and 5 others liked your photos"
aggregate([]Event) -> []PushNotif
State {
LastSentTime
CurrentEta
}
Event {
For // (Could be a userid or a phone number)
Type //eg. like
From // (Optional from userid)
About // (id of the item this notif is about, eg. photo id)
CreatedAt
Extra // (any extra information associated with this event)
}
PushNotif {
Message // Message to be sent
Args //key value pairs to send along with the notif
SendTo // Send to this userid or phone number
ForOs // To restrict it to only ios or only android
}

Implementation

The notif engine is implemented using a message queue . Our app is built on app engine so we use the built in push queue but it can be used with any message queue which supports scheduling a task in the future. We use 3 queues for this: processor, aggregator and sender. When the app fires an event for a receiver eg. “Send like event to user B”, it gets put on the processor queue. The engine then processes this event, calculates the eta and then schedules this event type for the given receiver on the aggregator queue. This then gets executed at the given time and calls the aggregate method which outputs notification payloads which get put on the sender queue. Consumers of the sender queue are responsible for maintaining connections with apple and google.

Other advantages

Designing the engine in this way also allows us to do other things with notifications:

  1. Easy cancellation of notifications: When the user opens the app, we can just fire a cancel event which prevents the accumulated notifications from sending.
  2. Notification permissions: We can handle notification permissions in the engine as well. If the user wants to switch off certain notifications, we can do that without complicating our app code.
  3. Not sending notifications at night: Since the time aggregation is separate, we can easily modify the eta method to not send notifications to users at night, again without over complicating app code or the notif aggregation part.
  4. Notification activity feed: Since each notification event is stored, we can use the same data source for generating the users notification feed.
  5. Giving users control: The user can dictate how frequently they want certain notifications and this just requires modifying just the eta method.

Conclusion

The app landscape has matured and users want refined experiences. For a lot of apps, notifications play a very important role and having a great notifications experience is paramount for user engagement. By designing systems like these, small teams can rapidly iterate and provide awesome user experiences without repeating effort.

--

--