Mobile Push Notification Architecture
Design push notification systems that engage without annoying users. Covers platform-specific delivery (APNs, FCM), notification channels, rich notifications, delivery tracking, and the patterns that maximize open rates while respecting user attention.
Push notifications are the most powerful and most abused channel in mobile engineering. Well-designed notifications drive engagement — a timely alert about a package delivery or a price drop on a watched item creates genuine value. Poorly designed notifications — spam, irrelevant updates, middle-of-the-night pings — get your app uninstalled.
Architecture
Push Notification Pipeline:
Your Backend → Push Delivery Service → Platform Gateway → Device
Step 1: Server decides to send notification
Trigger: Business event (order shipped, message received)
Personalization: User preferences, timezone, quiet hours
Targeting: Specific user/device or segment
Step 2: Push delivery service
┌──────────────────────────────────────────────┐
│ Push Service (Firebase, OneSignal, etc.) │
│ │
│ Token management: │
│ Device registers → receives push token │
│ Token stored in your backend │
│ Tokens refresh → need re-registration │
│ │
│ Platform routing: │
│ iOS tokens → APNs (Apple Push Service) │
│ Android tokens → FCM (Firebase Cloud Msg) │
│ Web tokens → Web Push (VAPID) │
└──────────────────────────────────────────────┘
Step 3: Platform gateway delivers to device
APNs: Requires .p8 auth key or .p12 certificate
FCM: Requires service account or API key
Step 4: Device receives and displays
Foreground: Handle in-app (banner, in-app message)
Background: System notification tray
Killed: System wakes app briefly for processing
Implementation
class NotificationService:
"""Production notification service with best practices."""
def send_notification(self, user_id: str, notification: dict):
"""Send a smart, personalized push notification."""
user = self.get_user(user_id)
# 1. Check user preferences
if not user.notifications_enabled:
return {"status": "skipped", "reason": "notifications_disabled"}
category = notification["category"]
if category not in user.enabled_categories:
return {"status": "skipped", "reason": f"category_{category}_disabled"}
# 2. Respect quiet hours
user_time = self.get_local_time(user.timezone)
if self.is_quiet_hours(user_time, notification.get("priority", "normal")):
# Queue for delivery after quiet hours
self.schedule_for_later(user_id, notification,
after=user.quiet_hours_end)
return {"status": "scheduled", "deliver_at": user.quiet_hours_end}
# 3. Rate limiting
recent_count = self.get_recent_notification_count(
user_id, window_hours=24
)
if recent_count >= user.daily_limit:
return {"status": "throttled", "reason": "daily_limit_reached"}
# 4. Send to all user devices
devices = self.get_user_devices(user_id)
results = []
for device in devices:
if device.platform == "ios":
result = self.send_apns(device.token, notification)
elif device.platform == "android":
result = self.send_fcm(device.token, notification)
results.append(result)
return {"status": "sent", "devices": len(results)}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Send everything to everyone | Users disable notifications or uninstall | Category preferences, user segmentation |
| No quiet hours | 3 AM notifications = angry users | Timezone-aware quiet hours |
| No delivery tracking | No idea if notifications are received | Track delivery, open, and dismiss rates |
| Identical notification across platforms | Misses platform-specific features | Platform-optimized content and rich media |
| No notification deduplication | Same event triggers multiple notifications | Dedup key per event, collapse ID |
Push notifications are a privilege granted by the user. Every notification that does not provide genuine value erodes that trust. Design for relevance, respect for attention, and measurable engagement.