Designing Mobile-Friendly APIs: Idempotency, Pagination & Resilience

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 moments, get killed by the OS, and run on devices with wildly different performance profiles. If our backend API is designed like a typical web API, our app eventually hits 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 has to treat every write as potentially repeated, make every read/list incremental and resumable, provide clear stable error semantics, and stay backward compatible for older app versions.

Architecture pattern: smart server, resilient client

The mobile app sits on top of an offline cache (Isar), a repository layer, and a mobile API client, which talks to an API Gateway handling auth, an idempotency layer, business services, and the database. Key principle: the server handles correctness (idempotency, validation), and the client handles resilience (retry, backoff, caching).

Idempotency: preventing double actions

The problem: a user taps "Pay" twice, the OS retries after resume, and the network drops right after the request reaches the server. The fix is idempotency keys. The 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.

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.

Offset-based pagination (?offset=20&limit=20) is fragile once lists start mutating, new inserts shift the offsets. Use cursor or 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 as new items get inserted, and makes infinite scroll trivial to implement. For feeds or logs, also 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."

Retries and timeouts: surviving bad networks

Retrying is essential on mobile, but naive retrying without idempotency is dangerous. On the client, set appropriate timeouts, 5-10s for connection, 15-30s for reads depending on the endpoint, and implement exponential backoff with jitter: retry delays like 1s, 2s, 4s, 8s, capped around 20-30s total. Don't retry on 4xx errors or explicit "do not retry" server codes.

The magic combination for write operations: always send an idempotency key, and allow retries from user action, network resume, or library-level retry logic. That gives you correctness plus resilience.

Stable error contracts

Mobile clients need predictable error responses, you can't change error formats every release since older app versions still exist. A good error shape:

{
  "status": "error",
  "code": "CARD_DECLINED",
  "message": "Your card was declined. Try another payment method.",
  "retryable": false,
  "details": {
    "reason": "insufficient_funds"
  }
}

The client uses code to map to UX, retryable to decide whether to show a retry button, and message as fallback text.

Versioning and backward compatibility

Mobile apps update slowly, some users will be a year behind. Guidelines: prefer backward-compatible changes (add fields, don't remove them), version key endpoints when breaking changes are necessary (/v1/orders, /v2/orders), use feature flags to disable features gracefully for older versions, and never break old versions overnight.

The mobile-friendly API checklist

  • Every important write is idempotent, requiring idempotency keys on payment/order endpoints.
  • All list endpoints use cursor pagination: items + next_token + has_more.
  • Clients implement retries with backoff, retrying only idempotent operations.
  • Errors are consistent and structured with code, message, and retryable.
  • Timeouts are designed for mobile; fast fail gives users control back.
  • Versioning is cautious; never silently break old clients.
  • Observability is built in; logs include IDs, tokens, user, and 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 isn't.

This architecture lets Hoomanely 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 keeps payments from ever processing twice, keeps insights, logs, and device readings syncing consistently, keeps pet histories accurate across devices, keeps the app responsive on weak or fluctuating networks, keeps the IoT bowl syncing reliably in all conditions, and keeps order and delivery statuses trustworthy and up to date.