PATTERN Cited by 1 source
Migrate function for component downgrade¶
Problem¶
You bumped a component's major version because you needed to change the type of one of its fields. Old clients in the field don't understand the new shape and can't be forcibly upgraded. How does the backend serve both?
Pattern¶
At every breaking change (major version bump), ship a migrate function that takes an instance of the new major version and returns an instance of the previous major version:
Auto-generate the stub as part of the same codegen
pipeline that produces the component bindings (see
patterns/single-json-spec-to-multi-platform-codegen).
Make the stub's default behaviour fail closed: throw
NotSupportedError or equivalent so forgetting to implement
the migration is loud, not silent. Developers implement the
migration per-field for the new version.
At serialisation time, the backend decides — based on the
client's spec version — that
the client speaks V<N> but the backend wants to send
V<N+1>. The runtime invokes migrate() to produce the
V<N> instance and serialises that.
build view: CookbookButtonV1(text=FormattedText(...))
│
│ client spec: android@23.0
│ spec says Button max version = "0.8"
│
▼ runtime calls migrate()
CookbookButtonV0(text="Click me to go to Yelp", ...)
│
▼
serialise → old client can deserialise
Canonical wiki instance (Yelp Konbini)¶
Konbini's Button example:
def migrate(model: CookbookButtonV1) -> CookbookButtonV0:
"""
The migrate function defined here is meant to backport an
instance of CookbookButtonV1 to an instance of
CookbookContainerV0. Its default behavior is to throw an
error signifying that backporting the model is not
supported. However, we highly recommend replacing this
behavior with your own custom logic ...
"""
return CookbookButtonV0(
text=model.text.toString(), # V1 FormattedText -> V0 String
style=model.style,
on_click=model.on_click,
size=model.size,
background_color=model.background_color,
)
The pattern hinges on text=model.text.toString() — a
per-field, domain-aware backport. Losing the http-link
information in FormattedText is acceptable here because the
V0 client couldn't have rendered links anyway; the
string-only representation is strictly less rich but
strictly understandable.
Yelp: "This method is called whenever the spec version on the client is lower than the one on the backend. In that case, the backend tries to instantiate a CookbookButtonV1 class but it realizes the client only supports CookbookButtonV0."
Key properties¶
- Fail-closed default. The generated default throws. If a
developer adds a breaking change but forgets to implement
migrate(), the backend's feature error wrapper drops the feature rather than crashing the view — a correct, conservative fail mode. - Explicit opt-out. Not implementing
migrate()is an explicit choice to drop older clients for this feature. No accident; no silent data-shape mismatch. - Field-level fidelity decisions. Losing information is frequently unavoidable when downgrading; the migrate function is where each field's loss is documented as code.
- Co-lives with the new major. Both
V0andV1classes exist in the backend codebase;migrate()lives onV1. Old clients keep working; new clients get new capabilities; one codebase.
Open questions / caveats¶
- Cross-major chains. The post shows only
V1 → V0. IfV2.0ships while some clients still pin specs atV0, can the migrations compose (V2 → V1 → V0) or does each pair need its own pairwise function? Not stated. A safe implementation probably composes; Konbini's exact choice is undocumented. - Migration-function tests. Because a bad migration silently renders the wrong thing on older clients, they're the kind of code that most deserves per-field unit tests — but the post doesn't say whether Yelp enforces test discipline here.
- Removing a major. Eventually you want to delete
V0. The pattern doesn't by itself say when that's safe — you need fleet visibility on which spec versions are still deployed.
Composes with¶
- patterns/spec-version-negotiation-for-backward-compat — migrate is the fallback invoked when spec-version negotiation finds the client can't speak the newest major.
- patterns/single-json-spec-to-multi-platform-codegen — codegen generates the stub migrate alongside the new major's bindings.
- concepts/breaking-change-requires-major-bump — the trigger that causes the migrate to exist at all.
Contrasts¶
- Schema-level field-compatibility conventions (Protobuf, Thrift, Avro) — prevent most breaking changes by convention. Migrate functions are a richer alternative for the cases where type-change is unavoidable.
- API versioning (
/v1/vs/v2/) — coarser unit of versioning (endpoint, not component). Migrate is the finer-grained equivalent at the component level. - No-migrate fail-open — some systems silently send unknown fields and hope older clients skip them. This pattern rejects that in favour of explicit, declarative fallback.
When to reach for this pattern¶
- Client fleet has upgrade lag (mobile apps, long-lived desktop installs).
- Interface schemas will plausibly need type changes or renames over time.
- You already have the codegen substrate (see
patterns/single-json-spec-to-multi-platform-codegen)
that makes adding
migrate()stubs nearly free.
When not to¶
- Interfaces are intentionally additive-only (pure field-compat convention suffices).
- Forced upgrade is acceptable (internal tooling).
- The engineering cost of maintaining per-component migrate chains outweighs the cost of dropping older clients.
Seen in¶
- sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms
— canonical wiki first source. Yelp Konbini auto-generates
throw-by-default
migrate()stubs on every major-version bump of a Cookbook component; developers implement them to backport to the previous major; the runtime invokes them when the client's bundled spec version allows only the older major.