# Building a Kotlin Multiplatform Data Pipeline for Tilt

How a custom data framework replaced complex coroutine plumbing with a unified source-based architecture.

- **Source:** [ProAndroidDev - Medium](https://brifing.si/source/d9553fa9-7b72-4b9b-bc73-c6bf9a40dd5f)
- **Published:** 2026-10-15
- **Topic:** [Software & Engineering](https://brifing.si/c/engineering)
- **Original article:** <https://proandroiddev.com/everything-is-a-source-part-1-0155cfd84c4d?source=rss----c72404660798---4>
- **On Brifing:** https://brifing.si/s/b9670523-e0ad-40dc-bd36-ab9536296e30

## The Challenge of Shared Logic

Tilt uses a single Kotlin Multiplatform module to manage networking, caching, and business logic for Android and iOS. Previously, this layer relied on hand-rolled coroutine plumbing. As the team grew in late 2025, the complexity of managing Flows and error handling led to inconsistent code patterns and skipped error checks.

## Limitations of Manual Flow Handling

The original codebase suffered from repetitive ladder-like structures using isSuccess and getOrThrow. Developers frequently forgot branches or incorrectly handled exceptions. Additionally, the existing combineTiltResult helper required new overloads for every input type combination, making the system difficult to scale and maintain as the application's data requirements expanded over time.

## Strategic Decision to Build

The team chose to build a custom framework rather than adopting an external SDK. This decision ensured full control over the underlying architecture and allowed for rapid iteration. By keeping the framework small and focused on wrapping inputs, combining them, and standardizing error handling, the team avoided unnecessary learning curves and dependencies.

## The Failed First Attempt

In November 2025, the team introduced a builder DSL to simplify data pipelines. While it looked clean on slides, it proved difficult to use in practice. IDE auto-completion was ineffective, and the runtime checks for source registration were brittle. The split between fetch and observe calls further confused the developers using the tool.

## Everything Is a Source

The January 2026 rewrite introduced the PipelineSource concept, where every data input is treated as a source. Under the hood, this is a Flow of TiltResult, ensuring that failures propagate as values rather than escaping as exceptions. This shift allowed developers to build, connect, and terminate pipelines without touching raw Flows.

## Standardizing Data Operators

The framework provides factory methods for Apollo calls, Flows, or plain values. Operators are named by intent, such as transform or validate, and consistently catch exceptions to turn them into typed failures. This design ensures that a checkNotNull call results in a manageable parsing error rather than a system crash.

## Type-Safe Source Combination

The new system replaced 11 manual overloads with fixed-arity functions like sourceCombine. These functions short-circuit on the first failure, ensuring the lambda only executes when all inputs succeed. For dynamic inputs, the team implemented combineSources, removing the need for manual result wrapping and significantly cleaning up the repository layer code.

## Default Propagation Rules

A core rule of the framework is that failures propagate by default to the consumer. Developers must explicitly decide to degrade functionality using fallback operators. This prevents silent outages where errors are swallowed, ensuring that developers remain aware of data staleness or service failures within the application's complex data streams.

## Explicit Degradation Patterns

In the home feed, the team implemented explicit degradation for non-critical data. By using timeout and withFallbackValue, the settings call can fail without blocking the feed. This approach makes the decision-making process visible in the code, distinguishing between a successful absent value and a genuine failure that requires logging.

## Refining the Cache Strategy

A migration of the live auction stream initially attempted to treat the stream as the cache. However, this caused performance issues with distinctUntilChangedBy. The team reverted to a boring cache that returns sources, which improved logging and error visibility compared to the previous imperative code that silently discarded failed fetches.

## Unexpected AI Agent Benefits

The framework was originally designed as guardrails for human developers unfamiliar with Flow nuances. Surprisingly, it proved equally effective for AI coding agents. Because the framework enforces a strict, readable structure, AI agents are less likely to fall back on unreliable training patterns, resulting in more robust and maintainable generated code.

## Impact of the Architecture

The transition to a source-based pipeline reduced 1,600 lines of complex plumbing into expressive, readable expressions. By standardizing error propagation and removing manual boilerplate, the team created a resilient foundation. This architecture now supports advanced features like OpenTelemetry tracing, proving that simple, consistent abstractions outperform complex, hand-rolled solutions in production environments.

## Sources covering this story

- https://proandroiddev.com/everything-is-a-source-part-1-0155cfd84c4d?source=rss----c72404660798---4 — 

---

_Summary written by Brifing from the original reporting. Copyright in the original article belongs to its publisher; read the full piece at the link above._
