Offline-First Sync Engine in SwiftData
Practice delivering a structured iOS system design answer using Excalidraw architecture diagrams.
Introduction
30 Second Summary
In a system design interview, the interviewer says: "Design an offline-capable reservation management app for hotel partners." You have 60 minutes to clarify requirements, lay out an architecture, deep-dive into components, and articulate tradeoffs. Most candidates freeze because they've never diagrammed the thing they're describing.
In this project, you will practice delivering a structured system design answer by diagramming the full architecture in Excalidraw. You will clarify requirements, draw layers, trace data flows, annotate tradeoffs at every decision point, and walk away with a visual artefact you can present to Cindy on Monday.
What You'll Build
You will walk through the same system design delivery framework you'd use in the interview, drawing each phase on a shared canvas: clarify requirements, define scope, present a high-level architecture, deep-dive into the offline-first layer and sync engine, then annotate tradeoffs and scalability considerations.
By the end of this project, you'll have:
- A complete architecture diagram you can present in the interview: five layers with labeled data flows, from SwiftUI views down to the backend API.
- A visual failure analysis showing exactly where and why an online-first approach breaks when connectivity drops, and how the local-first pattern resolves it.
- Annotated tradeoff decisions at every architectural fork: SwiftData vs Core Data, actor vs class for sync, last-write-wins vs field-level merge, optimistic vs pessimistic writes.
- Secret Mission: extend your diagram to cover multi-layer caching, background sync with BGTaskScheduler, and observability patterns that demonstrate scalability thinking.
Are there any prerequisites?
This project assumes you have completed the SwiftUI Partner Dashboard project (Project 2). You should be comfortable with iOS architecture concepts like @Observable, the repository pattern, SwiftUI navigation, and async/await. You need a browser and a free Excalidraw account.
Before We Start
Before we dive in, let's lock in the system design prompt you're preparing for and why this project gives you a first-hand answer to it.
Round 2 with Cindy evaluates structured problem-solving, architectural thinking, and tradeoff analysis. The domain (partner reservation management) is exactly what the SPX Run The Business team builds. By the end of this project, you won't be reciting theory. You'll be presenting a complete architecture diagram you drew and annotated yourself.
Set Up the Project and Data Models
Set Up Your Excalidraw Canvas
In a system design interview, the whiteboard is your medium. You need a structured canvas where you can draw layers, annotate tradeoffs, and trace data flows clearly.
In this step, you will open Excalidraw and set up a canvas divided into labeled sections that mirror the interview delivery framework. This gives you a visual workspace for every phase of the design.
In this step, get ready to:
- Open Excalidraw and create a new canvas.
- Divide the canvas into labeled sections for each interview phase.
- Verify your workspace is ready for the architecture exercise.
Open Excalidraw
- Open your browser and navigate to excalidraw.com.
- Click the + icon or press Ctrl+Shift+N (Windows) or Cmd+Shift+N (macOS) to create a fresh canvas.
Why Excalidraw?
Excalidraw produces hand-drawn-style diagrams that look natural on a whiteboard. It is free, runs in the browser, and exports to PNG or SVG. Many candidates use it to practice system design diagrams before interviews.
Create labeled sections on your canvas
Your canvas needs five distinct regions, one for each phase of the interview delivery. Draw these as large labeled rectangles arranged left-to-right or top-to-bottom:
- Select the Rectangle tool (press R) and draw a large rectangle in the top-left area of the canvas.
- Double-click the rectangle and type 1. Requirements & Scope.
- Draw four more rectangles alongside it, labeling them:
- 2. High-Level Architecture
- 3. Online-First Failure
- 4. Local-First Deep-Dive
- 5. Sync Engine & Backend
Use a consistent color scheme
Give each section rectangle a different light background color (yellow, blue, green, orange, purple). This makes it easy to see which phase each component belongs to when the diagram gets dense.
- You should now see five labeled, colored rectangles on your canvas. Each one will contain the diagrams you build in the following steps.
Canvas feels too small?
Use Ctrl+- (Windows) or Cmd+- (macOS) to zoom out. Excalidraw canvases are infinite, so spread your sections out with plenty of whitespace between them.
help me set up my Excalidraw canvas for a system design exercise
Your canvas is ready. Next up, you will fill the first section by diagramming requirements and scope boundaries.
Build the Online-First Version
Diagram Requirements and Scope
Your canvas is set up. Now you fill the first section with the content that opens every strong system design answer: requirements and scope.
In this step, you will draw a requirements table, a scope boundary diagram, and a data model sketch inside the first section of your canvas. These artefacts anchor every decision you make in later sections.
In this step, get ready to:
- Draw a requirements table with functional and non-functional requirements.
- Create a scope boundary diagram showing in-scope vs out-of-scope concerns.
- Sketch the data model with domain fields and sync metadata.
Draw the requirements table
Imagine Cindy says: "Design a mobile app for hotel partners to manage reservations offline." Your first move is to ask clarifying questions. On your canvas, you will document the requirements you extract.
- Inside your 1. Requirements & Scope section, draw a two-column table using rectangles and lines.
- Label the left column Functional and the right column Non-Functional.
- In the Functional column, add text boxes for each requirement:
- Partners view reservations (guest name, room, dates, status).
- Partners update reservation status (confirmed, checked in, checked out, cancelled).
- Status updates succeed immediately, even without network.
- Changes sync to server when connectivity returns.
- App shows sync state indicators (synced, pending, failed).
- In the Non-Functional column, add:
- Latency: <100ms for local writes.
- Reliability: no data loss on crash or network drop.
- Consistency: conflicts resolve deterministically.
- Scalability: supports adding messaging/reviews without restructuring sync.
Why clarify requirements first?
Requirements define what "done" looks like. Without them, you design a system that solves a problem nobody stated. In the interview, spending 5 minutes on requirements shows Cindy you think before you draw.
Create the scope boundary diagram
Equally important is what you explicitly exclude. Draw a clear visual boundary between what you will design and what you will not.
- Below your requirements table, draw two rectangles side by side.
- Label the left one In Scope (green border) and the right one Out of Scope (red border).
- Inside In Scope, add text: offline reads, offline writes, mutation queue, sync engine, conflict resolution (LWW), network monitoring.
- Inside Out of Scope, add text: auth/OAuth, push notifications, image caching, real-time WebSocket, multi-device conflict UI.
- Add an annotation below: "Core challenge = offline-first data flow. Auth and push are orthogonal."
How to scope in the interview
State your scope out loud: "I'll focus on the offline-first data layer and sync engine. I'll mention auth and push at the end as extensions, but I won't design them in detail." This shows Cindy you can prioritize under time pressure.
Sketch the data model
With requirements locked in, your first architectural decision is the data model. Every syncable entity needs two categories of fields: domain data and sync metadata.
- Below your scope diagram, draw a rounded rectangle labeled Reservation.
- Inside it, draw a dividing line and label the top section Domain Fields and the bottom section Sync Metadata.
- Add domain fields as text: id (UUID), guestName, checkInDate, checkOutDate, status, roomNumber, totalAmount, guestMessage.
- Add sync metadata fields: updatedAt, syncState (synced/pending/failed), version (Int).
- Draw a second smaller entity labeled SyncOperation with fields: id, reservationId, operationType, payload, createdAt, attemptCount.
- Draw a dashed arrow from SyncOperation to Reservation labeled "references".
Tradeoff: why inline sync metadata?
updatedAt enables last-write-wins conflict resolution. syncState tells the UI and sync engine which records need attention. version detects stale pushes.
Alternative: a separate "changelog" table. The tradeoff is query simplicity (one fetch gives you everything) vs separation of concerns (domain model stays clean). For a mobile app with a small dataset, inline metadata wins on simplicity. Add a text annotation on your diagram noting this tradeoff.
💡 Tradeoff: SwiftData vs Core Data vs SQLite
SwiftData wins here because: (1) the app targets iOS 17+ (matching Expedia Partner Central's minimum), (2) it integrates natively with SwiftUI and @Observable, (3) simpler setup with no .xcdatamodeld files.
Core Data would be right if you needed custom NSMergePolicy or targeted iOS 16. SQLite directly would suit a cross-platform engine shared with Android. Add an annotation to your data model boxes: "SwiftData (iOS 17+) — simpler than Core Data, native SwiftUI integration."
- Your first canvas section should now contain: a requirements table, a scope boundary diagram, and a labeled data model with two entities and a tradeoff annotation.
You now have requirements, scope boundaries, and data models documented visually. Next up, you will draw the high-level architecture that connects these models into a layered system.
Watch It Break Offline
Draw the High-Level Architecture
Requirements and scope are documented. The next interview move is presenting your high-level architecture: drawing the layers, naming the components, and showing how data flows between them and the backend.
In this step, you will draw the five-layer iOS architecture with arrows showing data flow from the SwiftUI view layer all the way down to the backend API. This is the diagram you would sketch on a whiteboard while explaining your approach to Cindy.
In this step, get ready to:
- Draw five architectural layers with labeled boxes.
- Add directional arrows showing the data flow.
- Annotate the key architectural principle.
Draw the five-layer stack
Navigate to your 2. High-Level Architecture section. You will draw five boxes arranged vertically (top to bottom), representing the layers of the iOS app, plus a sixth box off to the right representing the backend.
- Draw a rectangle at the top and label it View Layer (SwiftUI).
- Below it, draw a second rectangle labeled ViewModel (@Observable).
- Below that, draw Repository.
- Below the Repository, draw two boxes side by side: Local Store (SwiftData) on the left and Sync Engine (Actor) on the right.
- Off to the far right, draw a cloud-shaped or dashed-border box labeled Backend API.
Add data flow arrows
Now connect the boxes with labeled arrows to show how data moves through the system.
- Draw a downward arrow from View to ViewModel and label it "reads state".
- Draw a downward arrow from ViewModel to Repository and label it "fetch / update".
- Draw a downward arrow from Repository to Local Store and label it "read/write (always)".
- Draw a dashed arrow from Repository to Sync Engine and label it "enqueue mutation".
- Draw a dashed arrow from Sync Engine to Backend API and label it "push changes / pull updates".
- Draw a return arrow from Backend API back to Sync Engine labeled "server response + version".
- Draw an arrow from Sync Engine to Local Store labeled "merge server data".
Use dashed lines for optional paths
Solid arrows represent paths that always execute (local reads/writes). Dashed arrows represent paths that depend on network availability (sync to backend). This visual distinction immediately communicates the offline-first principle.
Annotate the key architectural principle
- Below the diagram, add a text annotation in a distinctive color (red or orange):
- Type: "KEY PRINCIPLE: If you rip out the Sync Engine entirely, the app still works as a local-only tool. The network is an optimization, not a dependency."
- Draw a box around this text to make it stand out as the core design thesis.
Why this principle matters in the interview
This single sentence communicates the most important architectural decision to Cindy. It proves you understand decoupling. The View never touches persistence. The Repository never touches the network. The Sync Engine has no reference to views. Each layer has exactly one job.
💡 Why an actor for the Sync Engine?
The Sync Engine is an actor (not @MainActor) because sync work should not block the UI thread. It creates its own ModelContext from the shared ModelContainer for background operations.
Alternative: a plain class with DispatchQueue. The tradeoff is that actors provide compile-time data-race safety, while a class-with-queue requires manual discipline. Add a text annotation to the Sync Engine box: "actor (not class) for compile-time thread safety."
- Your architecture section should now show five app layers, one backend box, labeled directional arrows between all components, and the key principle annotation at the bottom.
You have a complete high-level architecture diagram. Next up, you will trace what happens when the network disappears and show exactly where the online-first approach breaks down.
Architect the Local-First Repository
Trace the Online-First Failure
Your architecture diagram shows how data flows when everything works. But system design interviews are about what happens when things break. Cindy wants to see that you understand failure modes before you present your fix.
In this step, you will draw the online-first data flow, sever the network connection visually, and annotate exactly where and why each failure occurs. This gives you the concrete interview story: "I traced the naive approach, identified four failure modes, then redesigned the architecture to solve each one."
In this step, get ready to:
- Draw the online-first (naive) data flow path.
- Sever the network connection and annotate failure points.
- Document four distinct failure modes.
Draw the online-first data flow
Navigate to your 3. Online-First Failure section. You will draw a simplified version of the architecture showing the naive (online-first) approach where the app depends on the network for every operation.
- Draw three boxes in a vertical line: View, ViewModel, API Service.
- Draw a solid arrow from View to ViewModel labeled "loadReservations()".
- Draw a solid arrow from ViewModel directly to API Service labeled "fetchReservations()".
- Draw a solid arrow from API Service to a Backend box labeled "HTTP GET /reservations".
- Draw a return arrow from Backend back through API Service to ViewModel labeled "JSON response".
- Add a text label above this flow: Online-First: Network is the single source of truth.
What makes this approach naive?
Every read and every write goes through the network. The local store (if it exists) is only a cache that the app populates after a successful network response. If the network is unavailable, the app has nothing to show.
Sever the network and annotate failures
Now you show what happens when connectivity drops. This is the designed obstacle you would walk Cindy through.
- Draw a large red X across the arrow between API Service and Backend.
- Add a lightning bolt or "NETWORK DOWN" label in red next to the X.
- Now annotate four failure points with numbered red circles and text boxes:
- Failure 1: Draw a red circle on the View box. Add text: "No data displays. Partner sees 'Unable to Load' with wifi.slash icon."
- Failure 2: Draw a red circle on the ViewModel. Add text: "Status updates cannot persist. Changes are lost because there is no local write path."
- Failure 3: Draw a red circle below the View. Add text: "No meaningful error guidance. Partner has no idea when service will return."
- Failure 4: Draw a red circle near a small "cache" label. Add text: "Previously fetched data is unreachable. App reads from network first, ignoring the local store."
Why this matters for your interview
These are the exact problems the SPX Run The Business team solves daily. Partners use the Expedia Partner Central app on the go, in hotels with spotty Wi-Fi, and in areas with poor cell service.
When Cindy asks "how would you handle offline scenarios?" you can point to this diagram and say: "I traced the failure modes first, then designed the fix."
Add the fix preview arrow
- At the bottom of this section, draw a bold arrow pointing to the right (toward your next canvas section) labeled: "FIX: Make SwiftData the source of truth. Read locally first. Network becomes optional."
- This arrow visually connects the problem (this section) to the solution (the next section).
- Your failure analysis section should now show the naive data flow, a severed network connection with a red X, four numbered failure annotations, and a preview arrow pointing to the fix.
You have documented exactly why online-first architecture fails for this use case. Next up, you will draw the local-first repository pattern that solves each of these failure modes.
Build the Sync Engine
Design the Local-First Repository and Sync Engine
You have shown where online-first breaks. Now you present the fix: the local-first repository pattern and the sync engine that processes queued mutations. In the interview, this is the deep-dive where Cindy probes your reasoning hardest.
In this step, you will draw the local-first write flow, the mutation queue, the sync state machine, and the sync engine's retry and conflict resolution logic. You will annotate tradeoffs at every decision point.
In this step, get ready to:
- Draw the local-first write flow with sync state transitions.
- Diagram the sync engine with retry policy and conflict resolution.
- Annotate tradeoffs: optimistic vs pessimistic, LWW vs field-level merge, exponential backoff vs fixed retry.
Draw the local-first write flow
Navigate to your 4. Local-First Deep-Dive section. The key change from the naive approach: every write goes to SwiftData first and returns immediately. The network is handled asynchronously in the background.
- Draw a sequence of boxes left-to-right: Partner taps Update → Repository → SwiftData (Local Store).
- Draw a solid green arrow from Repository to SwiftData labeled "1. Write locally (instant, <100ms)".
- Below SwiftData, draw a box labeled Mutation Queue (SyncOperation table).
- Draw an arrow from Repository to Mutation Queue labeled "2. Enqueue sync operation".
- Draw a dashed arrow from Mutation Queue to a Sync Engine box labeled "3. Process in background (async)".
- Draw a dashed arrow from Sync Engine to Backend API labeled "4. Push when network available".
- Add a large annotation: "UI returns after step 1. Steps 2-4 happen in the background. Partner never waits for network."
Tradeoff: optimistic vs pessimistic writes
Optimistic writes (what you drew): write locally, show success immediately, resolve conflicts later. The UI is instant but conflicts are possible.
Pessimistic writes (the alternative): wait for server confirmation before showing success. No conflicts, but the UI blocks on network latency. For a partner managing check-ins in an elevator, blocking is unacceptable. Add an annotation: "Optimistic wins — partner needs instant feedback on-the-go."
Draw the sync state machine
Each reservation record has a sync state that transitions through three values. This state drives the UI indicators the partner sees.
- Draw three circles in a row: synced (green), pending (orange), failed (red).
- Draw an arrow from synced to pending labeled "local write".
- Draw an arrow from pending to synced labeled "push success".
- Draw an arrow from pending to failed labeled "max retries exceeded (5 attempts)".
- Draw an arrow from failed back to pending labeled "manual retry".
- Below the state machine, add text: "UI shows: green checkmark (synced), orange arrows (pending), red triangle (failed)."
Diagram the sync engine internals
Now navigate to your 5. Sync Engine & Backend section. You will draw the internal logic of the sync engine actor and how it communicates with the backend.
- Draw a large box labeled SyncManager (actor).
- Inside it, draw two sub-boxes: pushPendingChanges() and pullRemoteChanges().
- Draw an arrow from pushPendingChanges to Backend API labeled "SyncOperationDTO[] → HTTP POST".
- Draw a return arrow from Backend API labeled "SyncResponse (success/conflict/version)".
- Draw an arrow from pullRemoteChanges to Backend API labeled "HTTP GET /reservations".
- Draw a return arrow labeled "ReservationDTO[]".
- Draw an arrow from pullRemoteChanges back to Local Store labeled "merge (skip .pending records)".
Tradeoff: exponential backoff vs fixed interval
Fixed interval (retry every 5s): simple but hammers a struggling server. Immediate retry: fast recovery for transient blips but causes thundering-herd on real outages.
Exponential backoff (2s → 4s → 8s → 16s, capped at 60s): backs off progressively, giving the server room to recover while still retrying quickly on first failure. Add annotation: "Exponential backoff, 5-attempt cap, max 60s delay."
Draw the conflict resolution flow
- Below the sync engine box, draw a decision diamond labeled "Server version > local version?".
- Draw a "Yes" arrow leading to a box: "Accept server data, mark .synced".
- Draw a "No" arrow leading to a box: "Keep local data, stay .pending, retry next cycle".
- Add an annotation: "Last-Write-Wins (LWW): compare updatedAt timestamps as tie-breaker."
Tradeoff: LWW vs field-level merge
Field-level merge (comparing individual properties and keeping the newest per-field) is more sophisticated but adds complexity that's hard to reason about in production. LWW gives a clear, predictable outcome: the most recent change always takes precedence.
Most production apps start with LWW and graduate to finer-grained resolution only when real conflict patterns demand it. Add annotation: "Start LWW, graduate to field-level if conflict data shows need."
- Add a final annotation connecting the Backend API to NWPathMonitor: "Network framework monitors connectivity. Triggers sync automatically on reconnect."
- Your two final canvas sections should now show: the local-first write flow with state machine (section 4), and the sync engine internals with retry policy, conflict resolution, and backend contract (section 5).
Your complete architecture diagram is done. You have traced the full system from requirements through high-level architecture, failure analysis, local-first repository, and sync engine with backend contract. This is the artefact you present to Cindy.
Secret mission
Extend the Design: Monitoring, Caching, and Scalability
Your core architecture diagram is complete, but Cindy will probe beyond the data flow. She evaluates scalability thinking, observability, and whether you can extend the architecture without restructuring it. Diagram network monitoring with NWPathMonitor, multi-layer caching, and how this architecture scales to multiple feature teams.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Your Excalidraw canvas with the full architecture diagram.
- Your free Excalidraw account (if you signed up to save your work).
Keep everything running
No action needed. Choose this if you want to keep your diagram for interview reference or continue refining it.
- Your Excalidraw canvas auto-saves in the browser. Bookmark it or export as PNG/SVG for safekeeping.
- There are no cloud resources or ongoing costs to manage.
Pause - I'll come back to this later
Export your canvas so you can return to it on any device later.
- In Excalidraw, click the menu icon (hamburger) in the top left.
- Click Export to file to save a .excalidraw file to your computer.
- You can re-open this file later at excalidraw.com to continue editing.
Delete - I don't want to use this again
Remove your canvas data from the browser.
- In your browser, clear site data for excalidraw.com. In Chrome: click the lock icon in the address bar, then Site settings, then Clear data.
- If you exported a .excalidraw file, delete it from your Downloads folder.
- If you created an Excalidraw account and want to remove it, visit your account settings on excalidraw.com.
Nice Work!
Nice Work!
Nice work! You just practiced delivering a complete mobile system design for an offline-first reservation app using architecture diagrams. You followed the same framework you'll use with Cindy on Monday: requirements → scope → architecture → failure analysis → deep-dive → tradeoffs → scalability.
You've learned how to:
- Diagram a complete offline-first architecture with SwiftData as the local source of truth, showing how the UI loads instantly and never blocks on network availability.
- Design a sync engine with a mutation queue, exponential backoff, retry caps, and last-write-wins conflict resolution — articulating tradeoffs at every decision point.
- Trace failure modes visually by severing the network connection in your diagram and annotating exactly where and why the online-first approach breaks.
- Secret Mission: Extended the architecture with NWPathMonitor auto-sync, multi-layer caching, and observability patterns for scalability.
Ready to quiz yourself?