PATTERN Cited by 1 source
Unified preference model for cross-client state¶
Unified preference model for cross-client state is the pattern of canonicalising a single preference hierarchy that applies across all clients (desktop, iOS, Android, web) with explicit named override points for legitimate platform-specific divergence — instead of letting each client evolve its own preference schema with post-hoc sync logic stitching them together.
The pattern collapses N per-client preference models + sync layer into 1 canonical model + override dimension. It pairs naturally with patterns/decouple-what-from-how-in-preferences (which gives the shape of each axis) and patterns/read-time-schema-translation (which gives a zero-downtime path from the legacy per-client schemas to the unified one).
Canonical instance: Slack Notifications 2.0¶
Canonicalised from Slack's 2026-03-19 How Slack Rebuilt Notifications post. Legacy state:
'desktop': everything | mentions | nothing // four axes
'mobile': everything | mentions | nothing // conflated
Each client had its own enum, "nothing" meant different things on different platforms, and settings did not sync reliably. Verbatim: "Desktop and mobile each had their own preference systems, with different options and behaviors. A 'nothing' setting on mobile meant something entirely different from 'Off' on desktop."
Post-rebuild — one unified hierarchy, same across every client:
What to notify you about: All new messages
Mentions and DMs (default)
Mute
Push notifications: desktop and mobile (default)
desktop only
mobile only
disabled
Advanced: mobile-specific customization
badge controls
The canonical part is shared. The Advanced section is the named override point — mobile-specific badge controls ("badge all unreads") exist because home-screen badges are a real-interaction-surface divergence on mobile, not because mobile has ambiguously different preference semantics.
Canonical verbatim: "The goal was simple: mobile should match desktop by default, with the option to override when needed."
Shape¶
Three load-bearing components:
- Canonical shared hierarchy — one preference schema (names, enums, default values) that applies to every client. Usually expressed as orthogonal axes (patterns/decouple-what-from-how-in-preferences): what the user wants awareness of, how they want it delivered, where (per-channel overrides), when (schedules / do-not-disturb).
- Explicit named override dimension — a structured
slot in the hierarchy for legitimate platform-specific
divergence. Not a free-form per-client bucket; each
override is a named field the unified schema knows
about (e.g.
mobile_badge_mode: all | mentions | dm-only). - Default-inherit-with-explicit-override policy — when a preference is set, the default assumption is that other clients inherit it; overrides are explicit opt-ins at the named override dimension, not implicit defaults.
When to use it¶
Use this pattern when:
- Your multi-client product has drifted into per-client preference schemas with ambiguous cross-client semantics.
- Support tickets concentrate on "why don't my settings sync between devices?" or "I set this on desktop and mobile did something else."
- The product has acquired implicit sync parameters / heuristics that derive one client's state from another — canonicalised as the anti-pattern in concepts/explicit-state-over-implicit-sync.
- Platform-specific surfaces (home-screen badges, lock-screen previews, wearable notifications) exist but are being jammed into shared axes as if they were shared — producing non-composable semantics.
Do NOT use this pattern when:
- Legitimate preference surfaces are truly disjoint between platforms and the user expectation is per-platform independence (e.g. audio-output routing is not meaningfully a cross-platform preference).
- Your user base interacts with exactly one client at a time (browser-based product with no native apps) — no cross-client sync problem exists.
Implementation¶
Three implementation decisions decouple the migration risk from the canonicalisation benefit:
- Build the canonical schema first, treat legacy values as an adapter problem. Do not try to rewrite every client's storage simultaneously. Land the new schema at the read boundary via patterns/read-time-schema-translation — this is how Slack shipped Notifications 2.0 at millions-of-users scale without a database-level backfill ("With backwards compatibility and the possibility of rollback in mind, we thought it too risky to move people from 'off' to 'mentions' at the database level").
- Rewrite UI code against the canonical schema, not per client. Slack verbatim: "we built cross-platform consistency through reusable React components, replacing legacy mobile-specific UI code." Slack explicitly rewrote "some of the oldest pages in Slack's iOS app" to match desktop structure — accepting the mobile-rewrite cost to avoid permanent schema bifurcation.
- Name the override dimensions upfront. The "Advanced" slot isn't a free-form bag for future platform-specific prefs; it's a specific set of named fields (mobile badge mode, etc.) that the unified schema acknowledges. Unnamed overrides re-create per-client drift over time.
Failure modes¶
- Override-dimension sprawl. If every cross-platform friction becomes a new override field, the schema drifts back toward per-client. Discipline: override dimensions require legitimate structural divergence (home-screen badges on mobile are a real platform surface that doesn't exist on desktop); cosmetic per-client differences are UI-layer concerns, not schema concerns.
- Implicit sync re-introduction. Engineers sometimes
add "if mobile is unset, derive from desktop" sync
rules back into the client layer to save schema-width.
Canonical anti-pattern (see
concepts/explicit-state-over-implicit-sync): Slack
explicitly removed their legacy
syncparameter ("Clarity beats cleverness. Removing the sync parameter and storing explicit desktop and mobile values made behavior predictable."). - Migration-window bifurcation. During the migration window, some clients speak legacy + new and some speak new only. Without the read-time translator (patterns/read-time-schema-translation) as a safety net, client-version skew produces mismatched behaviour that is hard to debug.
- Override-dimension naming regret. Once an override field is named in the schema, rewriting it is itself a schema migration. Name override dimensions from first principles ("what is the interaction-surface divergence?") rather than from implementation-detail shorthand.
Related patterns¶
- patterns/decouple-what-from-how-in-preferences — the axis pattern; what each axis in the unified hierarchy should look like.
- patterns/read-time-schema-translation — the migration pattern; zero-downtime path from per-client legacy schemas to the unified model.
- patterns/expand-migrate-contract — canonical schema-migration pattern; Slack's approach is a variant where migrate is deferred indefinitely and read-time translation is the permanent answer.
- patterns/feature-flagged-dual-implementation — application-code analogue for running legacy and new implementations in parallel during rollout.
- concepts/cross-platform-preference-parity — the concept this pattern implements.
- concepts/preference-schema-decoupling — the shape each axis in the unified hierarchy takes.
- concepts/mental-model-preference-coherence — why the unified model is worth the migration cost.
Seen in¶
- Slack Notifications 2.0 (2026-03-19) — millions-of- users migration from four per-client enums (desktop / mobile × content-selection / delivery-channel) to one canonical hierarchy with "Advanced" as the named mobile-override dimension. Load-bearing migration mechanism is patterns/read-time-schema-translation; UI-layer mechanism is reusable React components replacing legacy mobile-specific code; "Advanced" is the only named override point. Support-ticket category (notifications) dropped out of Slack's top-3 CX drivers post-migration.
Source¶
- 2026-03-19 Slack Engineering, How Slack Rebuilt Notifications: https://slack.engineering/how-slack-rebuilt-notifications/ — via sources/2026-03-19-slack-how-slack-rebuilt-notifications.