androidengineers.Book a session

Designing an E-commerce App

Inventory Sync & Anti-Fraud

article20 minHard

E-commerce apps must show accurate stock levels without creating a poor user experience, and must protect against fraud signals that are detectable on the client side.

Inventory: Eventual Consistency Problem

True real-time inventory is expensive. Most apps use a compromise: show "approximate" stock, optimistically allow adding to cart, and validate at checkout.

data class StockInfo(
    val productId: String,
    val status: StockStatus,    // IN_STOCK, LOW_STOCK, OUT_OF_STOCK
    val quantity: Int?,         // show actual count only if < 10 (urgency cue)
    val updatedAt: Long         // freshness timestamp
)

enum class StockStatus { IN_STOCK, LOW_STOCK, OUT_OF_STOCK }

// Refresh stock for visible products as user scrolls
class InventoryManager(private val api: InventoryApi, private val cache: InventoryCache) {

    suspend fun getStock(productId: String): StockInfo {
        val cached = cache.get(productId)

        // Use cache if fresh (< 5 minutes)
        if (cached != null && System.currentTimeMillis() - cached.updatedAt < 5 * 60 * 1000) {
            return cached
        }

        // Refresh in background, return stale cache immediately
        scope.launch { refreshStock(productId) }
        return cached ?: StockInfo(productId, StockStatus.IN_STOCK, null, 0)
    }

    private suspend fun refreshStock(productId: String) {
        try {
            val fresh = api.getStock(productId)
            cache.put(productId, fresh.copy(updatedAt = System.currentTimeMillis()))
        } catch (e: IOException) {
            // Keep stale cache; don't crash product listing
        }
    }

    // Batch refresh for product list (single API call)
    suspend fun refreshBatch(productIds: List<String>) {
        val fresh = api.getBatchStock(productIds)
        fresh.forEach { cache.put(it.productId, it.copy(updatedAt = System.currentTimeMillis())) }
    }
}

Stock UI: Show Urgency

@Composable
fun StockBadge(stock: StockInfo) {
    when (stock.status) {
        StockStatus.OUT_OF_STOCK ->
            Text("Out of Stock", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelSmall)
        StockStatus.LOW_STOCK ->
            Text(
                if (stock.quantity != null) "Only ${stock.quantity} left!" else "Low stock",
                color = Color(0xFFE65100),  // deep orange — urgency
                style = MaterialTheme.typography.labelSmall
            )
        StockStatus.IN_STOCK -> {}  // don't show "In Stock" — it's assumed
    }
}

Client-Side Fraud Signals

The app cannot prevent fraud alone, but can send signals to the server for server-side decisions:

Device Fingerprinting (for Risk Scoring)

class DeviceRiskProfiler(private val context: Context) {
    fun getRiskSignals(): DeviceRiskSignals {
        return DeviceRiskSignals(
            // Emulator indicators
            isEmulator = isEmulator(),
            // Rooted device
            isRooted = isRooted(),
            // Screen interaction patterns (real users scroll; bots don't)
            userAgent = buildUserAgent(),
            // Play Integrity token (strongest device attestation)
            // Fetched separately via Play Integrity API
        )
    }

    private fun isEmulator(): Boolean {
        return (Build.FINGERPRINT.startsWith("generic") ||
                Build.FINGERPRINT.startsWith("unknown") ||
                Build.MODEL.contains("Emulator") ||
                Build.MODEL.contains("Android SDK built for x86"))
    }

    private fun isRooted(): Boolean {
        return arrayOf("/su", "/system/bin/su", "/system/xbin/su").any { File(it).exists() }
    }

    private fun buildUserAgent(): String =
        "Android/${Build.VERSION.RELEASE} ${Build.MANUFACTURER}/${Build.MODEL}"
}

data class DeviceRiskSignals(
    val isEmulator: Boolean,
    val isRooted: Boolean,
    val userAgent: String
)

OTP / 3DS for High-Value Orders

class CheckoutRiskManager {
    fun requiresAdditionalVerification(order: Order, riskSignals: DeviceRiskSignals): Boolean {
        return order.totalCents > 100_00 ||  // > $100
               riskSignals.isRooted ||
               riskSignals.isEmulator ||
               order.isFirstOrder ||
               order.shippingAddressDiffersFromBilling
    }
}

Rate Limiting UI

class AddToCartThrottler {
    private val clickTimes = LinkedList<Long>()
    private val maxClicksPerMinute = 10

    fun canAddToCart(): Boolean {
        val now = System.currentTimeMillis()
        // Remove clicks older than 1 minute
        while (clickTimes.isNotEmpty() && now - clickTimes.first() > 60_000) {
            clickTimes.removeFirst()
        }
        if (clickTimes.size >= maxClicksPerMinute) return false
        clickTimes.addLast(now)
        return true
    }
}

Key Takeaways

PatternRule
Inventory cache TTL5 minutes for product listing; always validate at checkout
Show scarcity"Only 3 left!" increases urgency and converts better
Batch stock requestsOne API call for the whole visible page; not N calls
Fraud signals to serverSend device signals; let server make fraud decisions
Play IntegrityStrongest client attestation; use for high-value operations
3DS for high-valueOrders > $100 or unusual risk signals → require additional authentication

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Inventory Sync & Anti-Fraud | Android System Design | Android Engineers