DEV Community

Cover image for KMP WorkManager: Enterprise-Grade Background Tasks for Kotlin Multiplatform
Viet, Nguyen Tuan
Viet, Nguyen Tuan

Posted on

KMP WorkManager: Enterprise-Grade Background Tasks for Kotlin Multiplatform

We all love Kotlin Multiplatform (KMP). Sharing business logic, networking, and data layers between Android and iOS is a dream come true. But let's be honest: there is one specific area where the "write once, run everywhere" dream often turns into a fragmented nightmare.

Background Processing.

If you are coming from Android, you are used to WorkManager. It's robust, persistent, and handles constraints like "only run when charging" beautifully.

Then you port your logic to iOS. You encounter BGTaskScheduler.

Suddenly, you realize:

  • Android tasks are persistent; iOS tasks are opportunistic and can be killed at any moment.
  • Android supports complex constraints; iOS is extremely strict (mostly just network).
  • The worst part: If an iOS user force-quits the app, your background task logic might be dead in the water, potentially corrupting data if you were halfway through a multi-step chain.

I spent months battling these discrepancies in enterprise projects. I wanted the reliability of Android's WorkManager, but on both platforms.

The existing libraries were mostly thin wrappers. They let you schedule a task, but they didn't solve the hard engineering problems of state restoration and OS resource limits.

So, I built KMP WorkManager.


The "Gap" in the Ecosystem

When I analyzed the existing solutions, I found they were missing critical "Enterprise-grade" features required for production apps:

Chain State Restoration: What happens if a multi-step workflow (Download → Process → Upload) gets killed by iOS at step 2? Most libs just restart from step 1 next time, wasting battery and data.

Reliability vs. Speed: Writing to UserDefaults or flat files on iOS is fine for small apps, but what if you have a queue of 500 offline analytics events?

Developer Experience: I didn't want to write Swift callbacks. I wanted a pure Kotlin API.

I decided to treat iOS constraints not as a bug, but as a feature to be managed gracefully.


Introducing KMP WorkManager

This isn't just a wrapper. It's a full-fledged task orchestration engine built for reliability.

Here is how you schedule a periodic sync task. Look familiar?

// Shared Code (CommonMain)
val result = scheduler.enqueue(
    id = "data-sync",
    trigger = TaskTrigger.Periodic(intervalMs = 15.minutes.inWholeMilliseconds),
    workerClassName = "SyncWorker",
    constraints = Constraints(
        requiresNetwork = true,
        // On Android, this uses Foreground Service
        // On iOS, this requests BGProcessingTask
        isHeavyTask = true
    )
)
Enter fullscreen mode Exit fullscreen mode

One API. Both platforms. Zero platform-specific boilerplate in your ViewModels.


Under the Hood: Engineering for Stability

I want to highlight three specific technical decisions that make this library production-ready.

1. The O(1) Append-Only Queue

On iOS, file I/O can become a bottleneck when managing persistent queues. In early versions, we saw performance hits when queueing hundreds of tasks because of O(N) serialization.

In v2.1.0, we rewrote the iOS persistence layer to use an Append-Only Queue architecture with atomic operations.

  • Enqueue: O(1) complexity (appending to file).
  • Dequeue: O(1) complexity (pointer arithmetic).

The Result: Benchmarks showed a 13x to 40x performance improvement over standard serialization methods. Enqueueing 100 chains dropped from ~1000ms to just 24ms.

2. Graceful Shutdown & State Restoration

This is my favorite feature. iOS gives background tasks about 30 seconds (or slightly more for processing tasks). If you run out of time, the system kills you.

We implemented a Graceful Shutdown mechanism. When iOS signals expiration:

  1. The library intercepts the signal.
  2. It creates a "savepoint" for your current Task Chain.
  3. It halts execution cleanly.
  4. Next time the app runs, it resumes exactly from the last successful step.

No data loss. No restarted downloads.

3. Transparent Exact Alarms

Android has AlarmManager for exact timing. iOS doesn't allow background code execution at exact times (only notifications).

Instead of faking it or failing silently, the library introduces ExactAlarmIOSBehavior. You decide explicitly how to handle this platform discrepancy:

scheduler.enqueue(
    id = "morning-reminder",
    trigger = TaskTrigger.Exact(time),
    workerClassName = "ReminderWorker",
    constraints = Constraints(
        // Explicitly handle iOS limitations
        exactAlarmIOSBehavior = ExactAlarmIOSBehavior.SHOW_NOTIFICATION
    )
)
Enter fullscreen mode Exit fullscreen mode
  • SHOW_NOTIFICATION: Just show a UI alert (Apple approved).
  • ATTEMPT_BACKGROUND_RUN: Try to run code (Best effort, not guaranteed).
  • THROW_ERROR: Fail fast during dev to catch logic errors.

My Commitment to Maintenance

This project is not a weekend hobby. It was born out of frustration in production environments and is built to stay.

  • Tests: The library currently has 236+ Unit & Integration Tests running on CI.
  • Documentation: We have extensive docs on migration, best practices, and architecture.
  • Roadmap: We are actively working on v2.2.0 (File Coordination Strategy) and v2.3.0 (SQLDelight integration).

Try it out

The library is available on Maven Central.

implementation("dev.brewkits:kmpworkmanager:2.1.2")
Enter fullscreen mode Exit fullscreen mode

I've included a comprehensive Demo App in the repo that simulates real-world scenarios like Chains, Failures, and Push Notifications so you can see it in action before integrating.

👉 Check out the GitHub Repository


I'd love to hear your feedback. If you are struggling with background tasks in KMP, give this a spin and let me know if it eases your pain!


Cover image credit: Generated with DALL-E, edited by author.

Top comments (2)

Collapse
 
tuan_nguyen_320fd3c5 profile image
Hung, Nguyen

Great article! I'm curious about the O(1) append-only queue implementation on iOS. Could you share more details about how you achieved 40x performance improvement? What was the previous approach and what changed?

Collapse
 
brewkits profile image
Viet, Nguyen Tuan

Thanks for the interest! Happy to dive deeper.

Previous Approach (v2.0.x):

We were using standard JSON serialization - loading the entire queue into memory, modifying it, then writing everything back.

This was O(N) for both enqueue and dequeue operations.

New Approach (v2.1.0+):

Implemented an append-only file structure with metadata tracking:

  • Enqueue: Append new task JSON to end of file (O(1))
  • Dequeue: Update header pointer, mark task as processed (O(1))
  • Automatic compaction at 80% "dead space" threshold

Benchmarks (iPhone 13 Pro):

  • Enqueueing 100 chains: ~1000ms → 24ms (40x faster)
  • Enqueueing 50 chains: ~500ms → 16ms (31x faster)

The key was avoiding full file rewrites. We use NSFileCoordinator for thread safety and atomic operations.

Check out IosFileStorage.kt and AppendOnlyQueue.kt for implementation details: github.com/brewkits/kmpworkmanager