Repository navigation
Mosaic 0.7.0 release notes
Mosaic 0.7.0 gives each request an explicit execution lifetime, distinguishes
borrowed Canvas values from owned resources, and isolates keyed failures. This
guide covers upgrading from 0.6.0; see compatibility before
updating runtime dependencies and optional analysis tooling.
Scoped request execution
Use Canvas.withMosaic as the canonical request entry point. Keep application
services in a long-lived Canvas, bind request input in a child Canvas, and run the
complete handler inside one scoped block:
import org.buildmosaic.core.injection.withMosaic
// In a suspending request handler; RequestKey and ResponseTile are your declarations.
applicationCanvas.withLayer {
instance(RequestKey, value)
}.withMosaic {
compose(ResponseTile)
}Tile producer work inherits the calling coroutine's dispatcher and context and
is scoped to that request. Producers are supervised: cancelling a consumer's
coroutine stops that consumer's wait without automatically cancelling shared
producer work. Cancelling the request cancels its unfinished producers. Leaving
withMosaic, normally or exceptionally, cancels unfinished speculative work and
waits for producer and attached-child cleanup.
Canvas.create() remains available but is deprecated at WARNING level.
Migration normally means moving the complete handler invocation into
requestCanvas.withMosaic { handler(this) }, rather than returning or retaining
a Mosaic between requests. Each block creates a fresh Mosaic and cache.
withMosaic does not close its Canvas. If the request child constructs owned
AutoCloseable resources, surround composition with use as shown in the
resource guide.
Canvas instance bindings
single { ... } constructs a Canvas-owned value. Canvas closes successfully
constructed local AutoCloseable values on close or construction rollback.
instance(existing) registers an already-existing, externally owned value:
Canvas must never close it, including when construction fails or is cancelled.
The external owner remains responsible for its lifetime.
The three forms mirror single. Each pair below shows alternatives for the same
binding: choose the owned constructor or the borrowed instance, not both.
single<Client> { Client() }
instance(existingClient)
single<Client>("primary") { Client() }
instance<Client>("primary", existingClient)
single(ClientKey) { Client() }
instance(ClientKey, existingClient)Use instance<Service>(existingService) when binding an interface or supertype;
instance("primary", existingClient) infers the type when that is the intended
binding. Positional CanvasKey bindings also work for String values, such as
instance(OrderKey, orderId). Each key can be registered only once per layer.
Instance bindings are available to constructors through paint and use the same
typed-key, qualifier, local-first lookup, and parent-fallback rules.
A child does not close parent-owned resources. Replace single { existingService }
with instance(existingService) when the application or framework owns the service.
MultiTile outcomes
Within one Mosaic, the same MultiTile retains one terminal outcome per equal
key, including failures. Repeating a composition is not a retry.
- A present map entry is successful even when its value is
nullandVis nullable. - A requested key omitted from the returned map fails that key with
NoSuchElementException; present sibling values remain retained. - A bulk provider invocation failure fails unfinished keys in that physical
invocation, preserving outcomes already published. perKeyTilefailures are isolated to that key.chunkedMultiTilefailures are isolated to that physical chunk's unfinished keys.- Successful sibling keys remain cached and are reused by later callers.
compose(tile, keys) is strict: it throws if a requested key fails, rather than
returning a partial map. composeAsync(tile, keys) returns Map<K, Deferred<V>>,
so callers can await and handle each key separately. Pending uncached keys can
coalesce before execution; exact physical batch boundaries are not guaranteed.
Request cancellation still cancels unfinished work across keys. See
MultiTile for the current contract.
Deferred ownership
Every composeAsync overload returns Mosaic-owned shared Deferreds. Callers
may await them freely. If a consumer no longer needs a result, cancel that
consumer's coroutine or wait. Directly cancelling a returned shared Deferred can
affect other consumers waiting for the same Tile or key. No wrapper or new result
abstraction is required.
Optional analysis
Analysis recognizes supported withMosaic execution and instance bindings.
Proven synchronous Tile result cycles produce
MOSAIC_CYCLIC_TILE_DEPENDENCY: a hard correctness error that cannot be configured
or suppressed. MOSAIC_RECURSIVE_TILE and MOSAIC_RECURSIVE_MULTITILE both default
to ERROR, but can be configured to ERROR, WARNING, or OFF.
Kotlin @Suppress is available for supported intentional local recursion.
Deferred-await linkage and MultiTile key equality/termination reasoning remain
outside the proof boundary. A configurable recursive finding is not proof of a
same-Mosaic cyclic result dependency. Use the canonical
analysis configuration reference
for rule definitions, suppression scopes, enforcement, and project restrictions.
Metadata changes from format 3 / analysis-contract-2 to format 4 /
analysis-contract-3. Old dependency summaries are incompatible and must be
regenerated with matching 0.7 analysis tooling. The resource path
META-INF/mosaic-analysis/v1/summary.json is unchanged; compatibility comes from
its header. Keep the Gradle and compiler plugins at the same Mosaic version.
Updated examples
The Spring, Ktor, and Micronaut examples demonstrate delegated,
named Tiles, Canvas-supplied application services, instance(...), child request
Canvases, and one withMosaic lifetime per request. Ktor uses coroutine routes;
Spring and Micronaut use suspending controllers. Synchronous application setup
uses runBlocking only to construct the application Canvas.
Runtime cost
The six changed scoped-drain and fully cached MultiTile fixtures have fresh
0.7.0 baselines, commands, and measurement boundaries.
Scoped diamond/sibling requests measured 2.743/4.036 µs/op; fully cached reads
measured 0.042–10.912 µs/op for 1–512 keys. Allocation was measured separately.
These fixtures are not directly comparable with their old versions; historical
0.5/0.6 tables and application measurements are unchanged.
Compatibility
- Runtime source/API:
withMosaicandinstanceare additions;
Canvas.create()is a WARNING deprecation, not a removal. Existing public
runtime method signatures remain available. Builds treating warnings as errors
may need to migrate deprecated calls. - JVM binary surface: the inspected Kotlin-public core API descriptors are retained.
The internal MultiTile provider representation changed, including its mangled
JVM getter; code calling internal members from Java or reflection can break.
This is not a general binary compatibility guarantee for 0.x. Recompile and
test consumers when upgrading, especially code using inlined DSLs. - Runtime behavior: scoped execution changes cancellation and cleanup;
MultiTile now retains per-key outcomes and successful siblings across provider
failures, and treats present nullable values as success. Review recovery,
cancellation, and resource ownership in your application. - Runtime requirements: Kotlin/JVM, Java 17 or later, and Kotlin consumer
compiler 2.3.0 or later remain the documented boundary. Dependencies remain
stdlib/kotlin-test 2.4.20, coroutines core/test 1.11.0, and OpenTelemetry API 1.66.0. - Optional analysis: format/semantic metadata is incompatible with 0.6
summaries. Only Kotlin compiler and Gradle plugin 2.4.20 are supported,
within the documented puremainKotlin/JVM boundary. This restriction does
not apply to runtime-only consumers.
For upgrades from earlier releases, also read the
0.6.0 Canvas migration. The runtime BOM
continues to align core, test, and OpenTelemetry; analysis tooling remains outside
it. See runtime and analysis requirements.
[0.7.0] - 2026-10-05
Added
Canvas.withMosaicfor one request lifetime: producer work inherits the calling
coroutine; every exit cancels unfinished work and waits for cleanup.- Typed, qualified, and CanvasKey
instance(...)bindings for externally owned
values that Canvas never closes, including on construction rollback. Qualifiers
precede values, mirroringsingle<T>("primary") { ... }. - Analysis recognition of supported
withMosaicandinstanceusage, proven
Tile-cycle detection, and configurable/suppressible recursive Tile/MultiTile
policy with ERROR/WARNING/OFF severities. Proven cycles cannot be disabled.
Changed
- MultiTile retains one terminal outcome per equal key. Per-key and chunk
failures preserve successful siblings; strict bulkcomposestill throws on
any requested failure. Returned Deferreds remain Mosaic-owned shared work. - Analysis metadata moves to format 4 /
analysis-contract-3; dependency
summaries must be rebuilt with matching 0.7 tooling. Optional analysis still
supports exactly Kotlin compiler/Gradle plugin 2.4.20. - Spring, Ktor, and Micronaut examples use named Tiles, Canvas-supplied services,
borrowed instances, child request Canvases, and suspending scoped handlers.
Deprecated
Canvas.create()at WARNING level. Move the complete handler invocation into
withMosaicrather than retaining a Mosaic across requests.
Fixed
- Present nullable MultiTile values succeed; omitted requested keys fail
individually without discarding present values. - Per-key and chunk provider failures no longer discard successful sibling keys.
- Tile body and attached-child failures use the same containment boundary with
and without execution observation.