androidengineers.Book a session

Designing an E-commerce App

Order Tracking & Notifications

article20 minMedium

Order tracking keeps users informed from purchase to delivery. Effective order tracking combines push notifications for important state changes with an in-app timeline that shows full history.

Order State Machine

enum class OrderStatus {
    PLACED,         // order confirmed, payment received
    PROCESSING,     // warehouse picking the order
    SHIPPED,        // handed to carrier
    OUT_FOR_DELIVERY,  // on the delivery vehicle
    DELIVERED,      // completed
    CANCELLED,      // cancelled before shipping
    RETURNED        // return processed
}

data class OrderStatusUpdate(
    val orderId: String,
    val status: OrderStatus,
    val timestamp: Long,
    val description: String,           // "Your order has been picked up by FedEx"
    val trackingNumber: String? = null,
    val carrierName: String? = null,
    val estimatedDelivery: Long? = null
)

Push Notifications by Event

class OrderNotificationService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        val orderId = message.data["order_id"] ?: return
        val status = message.data["status"]?.let {
            runCatching { OrderStatus.valueOf(it) }.getOrNull()
        } ?: return

        showOrderNotification(orderId, status, message.data)
    }

    private fun showOrderNotification(
        orderId: String,
        status: OrderStatus,
        data: Map<String, String>
    ) {
        val (title, body) = when (status) {
            OrderStatus.SHIPPED ->
                "Order Shipped!" to "Your order is on the way. Estimated delivery: ${data["eta"]}"
            OrderStatus.OUT_FOR_DELIVERY ->
                "Out for delivery" to "Your order will arrive today"
            OrderStatus.DELIVERED ->
                "Order delivered!" to "Your order has been delivered. How was it?"
            OrderStatus.CANCELLED ->
                "Order cancelled" to "Your order #${orderId.takeLast(6)} has been cancelled"
            else -> return  // don't notify for intermediate states
        }

        val intent = Intent(this, MainActivity::class.java).apply {
            putExtra("destination", "order_detail")
            putExtra("order_id", orderId)
        }

        val notification = NotificationCompat.Builder(this, "orders")
            .setSmallIcon(R.drawable.ic_package)
            .setContentTitle(title)
            .setContentText(body)
            .setContentIntent(PendingIntent.getActivity(
                this, orderId.hashCode(), intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            ))
            .setAutoCancel(true)
            .build()

        NotificationManagerCompat.from(this).notify(orderId.hashCode(), notification)
    }
}

Order Timeline UI

@Composable
fun OrderTimeline(updates: List<OrderStatusUpdate>) {
    val sortedUpdates = updates.sortedBy { it.timestamp }

    Column {
        sortedUpdates.forEachIndexed { index, update ->
            val isLast = index == sortedUpdates.size - 1
            val isCompleted = !isLast

            Row(modifier = Modifier.fillMaxWidth()) {
                // Timeline indicator
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier.width(32.dp)
                ) {
                    Box(
                        modifier = Modifier
                            .size(16.dp)
                            .clip(CircleShape)
                            .background(
                                if (isCompleted || isLast) MaterialTheme.colorScheme.primary
                                else MaterialTheme.colorScheme.surfaceVariant
                            )
                    )
                    if (!isLast) {
                        Divider(
                            modifier = Modifier.width(2.dp).height(48.dp),
                            color = MaterialTheme.colorScheme.primary
                        )
                    }
                }

                Spacer(Modifier.width(12.dp))

                Column(modifier = Modifier.weight(1f).padding(bottom = 16.dp)) {
                    Text(
                        update.status.displayName,
                        style = MaterialTheme.typography.titleSmall,
                        fontWeight = if (isLast) FontWeight.Bold else FontWeight.Normal
                    )
                    Text(
                        update.description,
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                    Text(
                        formatTimestamp(update.timestamp),
                        style = MaterialTheme.typography.labelSmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
            }
        }
    }
}

private val OrderStatus.displayName: String get() = when (this) {
    OrderStatus.PLACED -> "Order Placed"
    OrderStatus.PROCESSING -> "Processing"
    OrderStatus.SHIPPED -> "Shipped"
    OrderStatus.OUT_FOR_DELIVERY -> "Out for Delivery"
    OrderStatus.DELIVERED -> "Delivered"
    OrderStatus.CANCELLED -> "Cancelled"
    OrderStatus.RETURNED -> "Returned"
}

Polling Fallback (When WebSocket Unavailable)

class OrderTrackingPoller(
    private val orderId: String,
    private val repository: OrderRepository
) {
    fun poll(): Flow<OrderStatusUpdate> = flow {
        var lastStatus: OrderStatus? = null
        while (true) {
            val latest = repository.getOrder(orderId).statusUpdates.last()
            if (latest.status != lastStatus) {
                emit(latest)
                lastStatus = latest.status
                if (latest.status == OrderStatus.DELIVERED ||
                    latest.status == OrderStatus.CANCELLED) break
            }
            delay(60_000)  // poll every minute
        }
    }
}

Key Takeaways

PatternRule
Notify on key transitionsSHIPPED, OUT_FOR_DELIVERY, DELIVERED, CANCELLED — not every state change
Deep link from notificationTap → open order detail; use order_id in intent extras
Timeline UIShow all events in chronological order; latest is highlighted
Polling fallbackPoll every 60s if WebSocket unavailable; stop on terminal status
ETA formatShow "Estimated delivery: Mon, Jul 28" not just seconds

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Order Tracking & Notifications | Android System Design | Android Engineers