Skip to content
In development. Nothing in Store 6 is published yet.

Store 6 stability policy

1. What this document is

What each store6-* artifact promises, how an API is allowed to change, how often we ship, and how you can verify all of it from a released tag. Where a promise is not yet earned, this document says so rather than rounding up.

It is also the standing answer to #570 on binary compatibility and #534 on a published roadmap.

Scope: the store6-* artifacts, effective with the 6.0.0-alpha01 release. Store 5 continues under its own coordinates, and §6 covers living with both.

2. API tiers

Store 6 uses three opt-in markers. Each is a real annotation in store6-core, and the meaning below is the one carried in its own KDoc.

MarkerMeans
@ExperimentalStoreApiAPI under active development that may change or be removed in any release. Experimental API ships in separate artifacts wherever possible; the marker exists for the cases where an experimental member must live beside stable API.
@DelicateStoreApiAPI that is stable but easy to misuse: for example, implementing Store directly instead of building one through the store { } DSL. Opting in asserts that you uphold the documented contract of the marked declaration.
@InternalStoreApiAPI internal to the Store libraries. It may change or disappear without notice even in patch releases, and must never be used outside org.mobilenativefoundation.store artifacts.

All three are RequiresOptIn.Level.ERROR: you cannot use them by accident. Store additionally carries @SubclassOptInRequired(DelicateStoreApi::class), so implementing the interface yourself is a deliberate act, not a default.

Experimental code lives in separate artifacts, never annotation-gated inside a stable one. When a capability needs its own release rhythm, it gets its own artifact, and the tier is stated on the artifact rather than buried in an annotation on a member you have already depended on.

SemVer is scoped to the stable tier. A breaking change to an @ExperimentalStoreApi surface in a minor release is not a SemVer violation, because that surface never claimed the guarantee. That is the whole point of stating the tier on the tin.

3. Artifacts and tiers, as of 6.0.0-alpha01

Group coordinates are unchanged: org.mobilenativefoundation.store. Packages are org.mobilenativefoundation.store6.*.

ArtifactTierIn 6.0.0-alpha01
store6-coreStable-track. The API is not frozen until the beta01 freeze candidate.alpha01
store6-testingExperimental (@ExperimentalStoreApi): every public declaration in the artifact carries the marker today.alpha01
store6-sqldelightExperimental adapter (@ExperimentalStoreApi). Graduates to stable at 6.0.0, having run the contract kit throughout the alpha line.alpha01, may slip one alpha
store6-roomExperimental adapter, same graduation.alpha01, may slip one alpha
store6-composeExperimental adapter, same graduation.alpha01, may slip one alpha
store6-mutationsExperimental, separate artifact: every public symbol is @ExperimentalStoreApi. See §8.alpha01
store6-bomVersion alignment only; no API surface of its own.alpha01
store6-devtoolsExperimental (@ExperimentalStoreApi).alpha02 (target)
store6-devtools-inspectorExperimental (@ExperimentalStoreApi).alpha02 (target)

Inside store6-core, the org.mobilenativefoundation.store6.core.seam package (the 13 files you implement to plug in your own fetcher, source of truth, bookkeeper, clock, telemetry, or overlay) is a freeze candidate, not frozen. Today these types are @ExperimentalStoreApi, so implementing one is an explicit opt-in; that is the exception §2 names, and it is why the seam sits inside a stable-track artifact rather than shipping separately.

The candidate-versus-frozen distinction is load-bearing and we state it in two stages deliberately. A real producer has to exercise a seam end to end before we will call it a candidate. The Overlay and StoreWriteHandle surfaces become frozen only once the ack-path atomicity work and its test matrix are green; if that work misses beta01, those two ship @ExperimentalStoreApi outside the frozen tier and the rest of core freezes on schedule. CI enforces the 13-file list on every pull request, so the seam cannot grow quietly.

Promised: store6-store5-interop, tracking to 6.0.0 and not in the alpha01 line, and store6-paging-androidx, which joins the line in the first release it is green for. An artifact that misses a train gets its target release named here. It does not get dropped silently.

4. Deprecation cycle

Every removal from the stable tier goes through three stages:

  1. WARNING with ReplaceWith. The replacement is mechanical wherever the shape allows it.
  2. ERROR, no earlier than two minor releases later. You get at least two minors of warning before your build breaks.
  3. HIDDEN at the next major. Binary compatibility is preserved until then.

No silent capability drops. A removed capability gets the same cycle and a migration note. You should never find out a capability is gone by upgrading.

5. Release cadence

Monthly alphas from 6.0.0-alpha01. The governing rule is cut scope, never cadence: a slip threatens a release's contents, never its date. If something is not ready, it ships in the next alpha a month later and the release notes say so.

We will not repeat a 30-month alpha line, and we will not break API in beta again.

Each alpha closes at least one community issue with a link to the named guarantee that resolves it: a conformance test, not a changelog line. The next alpha's target month is stated in each release's notes. This document states the policy. Each release states the date.

The public roadmap is at ROADMAP.md.

6. Migrating from Store 5

store5.* and store6.* coordinates live side by side for the whole 6.x major. You can depend on both in one build and migrate a screen at a time. There is no flag day.

store6-store5-interop is supported for all of 6.x. The 5→6 and 4→6 migration guides are launch gates for 6.0.0. They block GA and are not follow-ups.

7. How stability is verified

Every claim in this document is checkable from a released tag.

  • explicitApi() strict on every store6-* library module. Nothing becomes public by omission.
  • Binary-compatibility-validator (0.17.0) with klib validation enabled. Each module commits a JVM .api dump and a .klib.api dump, for example, store6-core/api/jvm/store6-core.api and store6-core/api/store6-core.klib.api. The check runs as part of build on every pull request, so an unintended ABI change fails CI before review.
  • Generated-Swift dumps diffed on every pull request across the supported bridges: Obj-C export and SKIE today (store6-core/api/swift/objc, store6-core/api/swift/skie). The supported bridge set may change, so read this as a commitment to the mechanism rather than to a fixed list of lanes.
  • ABI dumps are committed at every released tag, so the surface of any release is diffable from the repository without resolving artifacts.
  • The conformance suite is public documentation of what is guaranteed. The behaviors this library promises are named tests you can read: store6-core/src/commonTest/kotlin/org/mobilenativefoundation/store6/core/ (*ConformanceTest.kt). When a release closes one of your issues, the notes link the test, not a bullet point.

8. Mutations at 6.0.0-alpha01

store6-mutations is in the alpha01 floor, not the may-slip list: an app that writes should not have to wait for a later alpha. Three things about it are worth stating plainly.

(a) The tier

Experimental, in its own artifact, every public symbol @ExperimentalStoreApi. The written graduation criteria are published alongside the 6.0.0-alpha01 release and linked from this section then; the first review is at 6.1. The target window for graduation to stable is roughly 6.3, and it is a target rather than a schedule: graduation requires the API unchanged across two consecutive minors, crash-matrix and soak lanes green in production-representative apps, and at least three external production adopters reporting. If those are not met, it stays experimental and the review repeats. Nothing graduates because a date arrived.

(b) The durable acknowledgement posture

Every public mutation store has a journal storage, including the in-memory default, which does not survive process restart. After the server returns an acknowledgement, Store records the receipt, any pending alias or tombstone, and the ACKED execution phase in one journal transaction. Only after it commits does Store adopt the result, apply effects, and finalize retirement.

If the server accepts a push before the local acknowledgement commits, the intent remains INFLIGHT; a later drain can resend the same immutable generation and key. Durable storage preserves that replay. This is the same conservative crash-window stance used for reads: prefer doing work twice over losing it.

Once ACKED is committed, recovery resumes adoption, effects, and retirement without calling MutationServer.push again for that generation. Those post-acknowledgement steps may repeat conservatively after a failure, but the accepted write is not sent twice from that durable phase.

The consequence: design mutation endpoints to treat a repeated idempotency key as the same request. This covers the remote-acceptance window before the local acknowledgement transaction commits.

(c) The current surface stays experimental

The entry point is the required-input mutationStore factory with an overlay-free builder, restart-safe key recovery is a compile-time-required resolver, the value state is an explicit presence algebra, and the persistence a caller installs is retained for the transactional ack-path decorator. The module remains experimental. Shapes can change in any release, and this document still deliberately freezes no mutations signature into policy prose.

9. Reading pending writes and staleness

Two affordances that look similar are not, and getting them backwards produces UI bugs that are hard to trace.

  • A "pending write" affordance keys on origin == OVERLAY.
  • A "stale cache" affordance keys on isStale.

isStale is never set on an OVERLAY frame. Overlay frames are fresh by definition: they are stamped age = Duration.ZERO and isStale = false unconditionally, because an optimistic value genuinely is new. The user just wrote it. On an overlay frame, only refreshing is live. So a spinner driven by isStale will never fire for a pending write, and that is intended. Drive the pending-write indicator off the origin and narrate the OVERLAYSOT flip.

Store.get is unprojected. Overlays apply only to stream, so an optimistic mutation is invisible to get. This is a documented consequence of the read contract, not a defect: get is a point read of committed truth. If you need to observe your own optimistic write, observe stream.

10. Kotlin floor

The store6 line requires Kotlin 2.3, raised only in minor releases and with notice.

The floor is what the published artifacts actually imply, not an aspiration: every published store6-core variant (JVM, Android, JS, wasmJs, and each native target) declares org.jetbrains.kotlin:kotlin-stdlib:2.3.20, and the build sets no apiVersion or languageVersion compatibility pin that would lower it.