Think from the response up.
Composable backend orchestration for Kotlin
Start with what your endpoint returns. Build it from small, reusable Tiles. Let independent branches share the work.
Mosaic is a Kotlin library for application logic and response data. Bring your own HTTP framework.
Mosaic 0.6.0 · Apache 2.0val OrderPageTile = singleTile { val summaryTile = composeAsync(OrderSummaryTile) val logisticsTile = composeAsync(LogisticsTile)
OrderPage(summaryTile.await(), logisticsTile.await()) }OrderPageTilecomposes summary + logisticsOrderSummaryTiledirectly needs OrderTileCustomerTile + LineItemsTileeach also needs OrderTileOrderTile shared by three branchesStart from what
the endpoint returns.
An order page needs a summary and shipping details. Each Tile builds its part of the response and asks for the dependencies it needs.
The endpoint stays small even when the graph behind it grows.
Tiles and compositionval OrderSummaryTile = singleTile { val orderTile = composeAsync(OrderTile) val customerTile = composeAsync(CustomerTile) val lineItemsTile = composeAsync(LineItemsTile)
OrderSummary(orderTile.await(), customerTile.await(), lineItemsTile.await()) }Independent work stays independent.
composeAsync starts each branch before you await its result. Use compose when the next step needs that value.
Batch naturally.
A MultiTile puts keyed work behind one reusable dependency. Overlapping requests reuse equal keys; newly pending keys can join a batch before execution starts.
Ready work is never intentionally delayed. Exact batch boundaries depend on scheduling.
Work with MultiTileval ProductsByIdTile = multiTile { keys -> ProductService.getProducts(keys.toList()) }product-1product-1 reusedproduct-2 fetchedSequential calls in one Mosaic. Only the new key reaches the fetch block.
Choose bulk, per-key, or chunked fetchingYour composition
is architecture.
The optional analysis plugin turns Tile relationships into a graph. Keep the code as the model, instead of maintaining a second diagram by hand.
./gradlew mosaicGraph
mosaicGraph output. Static dependencies, not runtime timing or batch boundaries.See what
actually executed.
Add Mosaic execution spans to your OpenTelemetry traces. Dependency links reveal shared producers and batch callers; cache hits create no new spans.
val applicationCanvas = canvas { tracing { openTelemetry }}Your application supplies OpenTelemetry, sampling, export, and shutdown.
Trace Tile executionTest response logic
without booting the world.
Replace dependency Tiles with fixture values and leave the real response composition running. Test inputs, failures, and virtual-time delays in isolation.
val testMosaic = mosaicBuilder() .withMockTile(OrderSummaryTile, summary) .withMockTile(LogisticsTile, logistics) .build()
testMosaic.assertEquals( OrderPageTile, OrderPage(summary, logistics))Inside runTest; summary and logistics are test fixture values.
Measured cost.
Visible tradeoffs.
Mosaic handles orchestration you would otherwise write yourself. The published application benchmark compares it with equivalent optimized Kotlin doing the same downstream work.
For the service-backed aggregate graph at 800 RPS, Mosaic added about 115 µs of CPU/request. Both implementations had median HTTP latency of about 21.12 ms.
Read the evidence and limitations| Workload | Mosaic − direct Kotlin |
|---|---|
| Light / batching | 14–21 µs |
| Aggregate graph | 48–115 µs |
| Sibling coalescing | 61–78 µs |
| CPU-heavy | 6–9 µs |
Rounded paired medians across measured profiles and rates. Source revision 129b0c7; application results were not remeasured for 0.6.0. CPU cost is not HTTP latency.
Bring your HTTP framework.
The same order Tiles run behind Spring Boot, Ktor, and Micronaut.
Keep routing and HTTP concerns in your framework. Compose the response with Mosaic.
Runnable framework examples. No published framework adapter is required.
Run an exampleBuild the response.
Compose the rest.
Add Mosaic to your Kotlin project.
Your first Tile is a few lines away.