Designing Mobile-Friendly APIs: Idempotency, Pagination & Resilience
Mobile apps don't live in the same world as web apps. They run on flaky networks, jump between Wi-Fi and 4G, go into the background at random, get killed by the OS, and are used on devices with wildly different performance profiles.
If our backend API is designed like a typical web API, our app will eventually hit duplicate payments, infinite spinner states, partially saved forms, and mysterious 500s after resume.
To build reliable mobile apps, we need a mobile-friendly API layer—endpoints and patterns that assume bad networks and still behave correctly.
What Makes Mobile Different?
Mobile clients deal with unreliable connectivity (tunnels, elevators), foreground/background churn, limited bandwidth, offline use, and multiple devices per user.
A good mobile API must:
- Treat every write as potentially repeated
- Make every read (list) incremental and resumable
- Provide clear, stable error semantics
- Be backward compatible for older app versions
Architecture Pattern: Smart Server, Resilient Client
Mobile App
└── Offline Cache (Isar)
└── Repository Layer
└── Mobile API Client
└── API Gateway
├── Auth
├── Idempotency Layer
├── Business Services
└── Database
Key principle: Server handles correctness (idempotency, validation). Client handles resilience (retry, backoff, caching).
1. Idempotency: Preventing Double Actions
The problem: User taps "Pay" twice, OS retries after resume, and network drops after the request reaches the server.
The solution: Idempotency keys.
Client generates a unique key per logical action:
POST /payments
Idempotency-Key: 7f1b1f2e-12ab-4b90-9cf7-3fd2f01e9af0
{
"user_id": "u_123",
"amount": 49900
}
Server logic (simplified):
def create_payment(request):
key = request.headers["Idempotency-Key"]
existing = idempotency_store.get(key)
if existing:
return existing.response # same result
result = process_payment(request.body)
idempotency_store.save(key, result)
return result
Make these operations idempotent: payments, orders, subscription changes, profile updates, form submissions—any "once-only" user action.
Even if the client is buggy or the network is unreliable, the server guarantees the action won't double-apply.
2. Pagination: List APIs That Scale
Mobile apps show feeds, notifications, orders, and chat history. You can't return everything in one response—it burns data, battery, and device memory.
Cursor-Based Pagination (Not Offset)
Offset-based pagination (?offset=20&limit=20) is fragile when lists mutate. New inserts shift offsets.
Use cursor/token-based pagination instead:
GET /orders?cursor=eyJpZCI6ICJvcmRfMTIzIn0&limit=20
Response:
{
"items": [...],
"next_token": "eyJpZCI6ICJvcmRfMTI0In0",
"has_more": true,
"server_time": "2025-11-25T10:00:00Z"
}
This stays stable even when new items are inserted and makes infinite scroll trivial to implement.
Time-Based Filtering
For feeds or logs, support time windows:
GET /events?after=2025-11-24T00:00:00Z&limit=100
This enables offline sync: "give me events after my last known timestamp."
3. Retries & Timeouts: Surviving Bad Networks
Retrying is essential on mobile, but naïve retrying without idempotency is dangerous.
Client-Side Strategy
Set appropriate timeouts:
- Connection timeout: 5–10s
- Read timeout: 15–30s (varies by endpoint)
Implement exponential backoff with jitter:
retry_delays = [1s, 2s, 4s, 8s... capped at ~20-30s total]
Don't retry on:
- 4xx errors (bad request, auth failures)
- Explicit "do not retry" server codes
The Magic Combination
For write operations:
- Always send an idempotency key
- Allow retries from user action, network resume, or library-level retry
This gives you correctness + resilience.
4. Stable Error Contracts
Mobile clients need predictable error responses. You can't change error formats every release—older app versions still exist.
Good error shape:
{
"status": "error",
"code": "CARD_DECLINED",
"message": "Your card was declined. Try another payment method.",
"retryable": false,
"details": {
"reason": "insufficient_funds"
}
}
Client uses code to map to UX, retryable to decide whether to show a retry button, and message as fallback text.
5. Versioning & Backward Compatibility
Mobile apps update slowly. Some users will be a year behind.
Guidelines:
- Prefer backward-compatible changes (add fields, don't remove)
- Version key endpoints when breaking changes are necessary:
/v1/orders,/v2/orders - Use feature flags to disable features gracefully for older versions
- Don't break old versions overnight
The Mobile-Friendly API Checklist
- Every important write is idempotent – require idempotency keys on payment/order endpoints
- All list endpoints use cursor pagination –
items + next_token + has_more - Clients implement retries with backoff – only retry idempotent operations
- Consistent, structured errors – with
code,message,retryable - Timeouts designed for mobile – fast fail gives users control back
- Cautious versioning – never silently break old clients
- Built-in observability – logs include IDs, tokens, user, app version
With these patterns in place, you get an API layer that behaves well under poor networks, doesn't double-charge, scales to large result sets, is debuggable when things go wrong, and supports multiple app versions safely.
Most importantly, it feels reliable to your users—even when their network is not.
This architecture allows Hoomanely to deliver dependable pet insights, frictionless checkout and order flows, precise IoT data ingestion, real-time timelines, and offline-ready experiences — all while preventing duplicate transactions or inconsistent records.
Our mobile-optimized API layer ensures:
- Payments are never processed twice
- All insights, logs, and device readings sync consistently
- Pet histories stay accurate across devices
- The app stays responsive on weak or fluctuating networks
- The IoT bowl syncs reliably in all conditions
- Order and delivery statuses remain trustworthy and up to date