DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Background Sync Engine Architecture (Reliable, Battery-Aware, Conflict-Safe)

Most apps implement sync like this:

refreshData()
Enter fullscreen mode Exit fullscreen mode

That works…
until you need:

  • offline edits
  • retries
  • background execution
  • conflict handling
  • battery awareness
  • exponential backoff
  • rate limiting
  • cross-tenant sync

At that point, sync becomes one of the hardest systems in your app.

This post shows how to design a background sync engine in SwiftUI that is:

  • reliable
  • resilient
  • battery-conscious
  • conflict-aware
  • testable
  • production-grade

🧠 The Core Principle

Sync is a state machine β€” not a network call.

If your sync logic lives in a ViewModel, it’s already fragile.


🧱 1. Define Sync States Explicitly

Never treat sync as boolean.

Bad:

isSyncing = true
Enter fullscreen mode Exit fullscreen mode

Correct:

enum SyncState {
    case idle
    case scheduled
    case syncing
    case failed(Error)
    case paused
}
Enter fullscreen mode Exit fullscreen mode

The engine transitions between states β€” predictably.


🧬 2. Sync Engine Lives in Infrastructure

final class SyncEngine {
    private let queue: SyncQueue
    private let api: APIClient
    private let persistence: PersistenceLayer
}
Enter fullscreen mode Exit fullscreen mode

It does not live in:

  • views
  • ViewModels
  • feature modules

Sync is cross-cutting infrastructure.


πŸ“¦ 3. Operation Queue Model

Treat sync work as operations:

struct SyncOperation {
    let id: UUID
    let type: OperationType
    let payload: Data
    let retryCount: Int
}
Enter fullscreen mode Exit fullscreen mode

Operations are:

  • persisted
  • retried
  • ordered
  • cancellable

Never fire-and-forget.


πŸ” 4. Persistent Sync Queue

Queue must survive:

  • app termination
  • device reboot
  • crashes

Store operations in local database.

On launch:

loadPendingOperations()
resumeProcessing()
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 5. Retry Strategy

Never retry infinitely.

Example:

func nextRetryDelay(for attempt: Int) -> TimeInterval {
    pow(2, Double(attempt)) // exponential backoff
}
Enter fullscreen mode Exit fullscreen mode

Rules:

  • max retry count
  • pause on fatal errors
  • respect HTTP status codes
  • differentiate network vs server errors

⚑ 6. Background Execution Integration

Integrate with:

  • BGTaskScheduler
  • background fetch
  • push-triggered sync

Example:

BGTaskScheduler.shared.register(...)
Enter fullscreen mode Exit fullscreen mode

Sync engine runs independent of UI.


πŸ”‹ 7. Battery & Network Awareness

Before syncing:

  • check reachability
  • check battery level
  • check low power mode

Avoid syncing:

  • on cellular (if heavy)
  • during low battery
  • while app inactive unless necessary

Sync must be respectful.


🧠 8. Conflict Resolution Integration

When server responds with conflict:

case .conflict:
    resolveConflict(local, remote)
Enter fullscreen mode Exit fullscreen mode

Resolution strategies:

  • last-write-wins
  • merge fields
  • server authority
  • manual resolution queue

Conflict handling must be centralized β€” not per feature.


πŸ§ͺ 9. Testing the Sync Engine

Mock:

  • API failures
  • network loss
  • partial success
  • background interruptions

Test scenarios:

  • retry exhaustion
  • queue recovery
  • duplicate prevention
  • idempotency

Sync bugs are subtle and expensive.


⚠️ 10. Common Sync Anti-Patterns

Avoid:

  • syncing from ViewModels
  • not persisting queue
  • retrying without backoff
  • ignoring conflicts
  • not handling app termination
  • duplicating operations

These lead to:

  • data corruption
  • server overload
  • angry users

🧠 Mental Model

Think:

Local Change
 β†’ Sync Operation
   β†’ Persistent Queue
     β†’ Retry Engine
       β†’ Server
         β†’ Reconciliation
Enter fullscreen mode Exit fullscreen mode

Not:

β€œJust refresh when needed”


πŸš€ Final Thoughts

A proper background sync engine gives you:

  • reliable offline behavior
  • predictable data consistency
  • fewer production bugs
  • better battery efficiency
  • scalable architecture

This is the difference between:

  • a demo app
  • and a real product

Top comments (0)