Skip to content

title: Decouple "what" from "how" in preferences type: pattern created: 2026-04-24 updated: 2026-04-24 tags: [preference-system, schema-design, decoupling, notifications, orthogonal-axes, user-settings, user-experience] sources: [2026-03-19-slack-how-slack-rebuilt-notifications] related: concepts/preference-schema-decoupling, concepts/mental-model-preference-coherence, concepts/separation-of-concerns, patterns/unified-preference-model-for-cross-client-state, systems/slack-notifications-2-0


Decouple "what" from "how" in preferences

Decouple "what" from "how" in preferences is the schema-design pattern of splitting a preference axis that conflates what to notify / show / deliver with how to notify / show / deliver into two orthogonal axes. Applies wherever a single preference enum forces users to trade off between content selection and delivery channel — most commonly in notifications but also in feed algorithms, digest settings, email/push/SMS channel selection, and any system where "content selection" and "delivery mechanism" are independent dimensions of user intent.

Canonical instance: Slack Notifications 2.0

Canonical verbatim from the 2026-03-19 How Slack Rebuilt Notifications post:

"What users were notified about was tightly coupled with how they received notifications. Wanting fewer push notifications meant sacrificing in-app awareness entirely — there was no way to separate the two."

"We decoupled 'what' from 'how,' giving users independent control over activity and push."

Slack's legacy notification preference conflated content selection (everything vs mentions vs nothing) with delivery channel (push on/off) into a single enum. Decoupling:

// Before: one enum, two axes conflated
'desktop': everything | mentions | nothing

// After: two axes, independent
'desktop':              everything | mentions   // What
'desktop_push_enabled': true | false             // How

Users can now express "mentions for in-app but no push" (a combination the legacy schema could not represent) and "everything in-app but push only for mentions" (another un-representable combination). The decoupling unlocks the cross-product of user intent.

Shape

The pattern decomposes a conflated preference enum into:

  1. Content axiswhat the user wants to be made aware of. Usually a discrete enum: all / important / none. This is the selection dimension.
  2. Channel axishow the user wants the system to deliver that awareness. Usually a boolean or a small enum: push / in-app-badge / email / SMS / none. This is the delivery dimension.
  3. Optional: per-context overrides — additional orthogonal axes for per-channel or per-platform granularity ("push off on mobile only", "channel-specific mentions on the highest-priority channel").

When to use it

Use this pattern when:

  • Users complain they can't express a specific combination of "content" + "delivery" intent.
  • Support tickets concentrate on confusion between "why am I getting notifications" (channel problem) and "I'm not seeing activity I care about" (content problem).
  • The legacy enum has none or off or nothing as a value — usually a tell that content and channel are conflated (since off typically means "neither-content-nor-channel" even though users might want one but not the other).
  • Analytics show users cycling between values experimentally trying to express a combination the schema can't represent.

Do NOT use this pattern when:

  • The axes are genuinely coupled — e.g. SMS delivery requires a phone number, and "content selection" is meaningless without a valid delivery channel.
  • The user base doesn't express differentiated preferences on the axes — most users pick a preset and never touch individual axes.
  • The additional preference surface would add confusion without unlocking expressive power.

Migration from conflated schema

The decoupling usually needs a schema migration of existing user preferences. Options:

  • Read-time translation — see patterns/read-time-schema-translation. Most rollback-safe; permanent translator cost. Slack's canonical choice.
  • Expand-migrate- contract — the six-step schema-migration discipline, ending in a cleanup step. More operational cost; cleaner end state.
  • Opportunistic migration — legacy values translated on write only (the next time the user touches their preferences). Combines with read-time translation as a passive cleanup strategy.

Implementation discipline

  • Default values for the new axis — when decoupling, what's the default for the newly-introduced axis? Slack's rule: backfill desktop_push_enabled = false for users with legacy 'nothing', true for everyone else. The default should preserve existing behavior exactly.
  • Write-path compatibility — during the migration window, writes must produce values both the legacy and new code paths can interpret. See the write-path discipline in patterns/read-time-schema-translation.
  • UI refactor follows schema — don't ship the decoupled schema with the legacy UI, or the new expressive power is hidden. Slack paired the schema split with a new modal exposing the two axes as distinct settings sections.

Relationship to separation-of-concerns

This pattern is concepts/separation-of-concerns applied at the user-preference-schema altitude. Standard separation-of-concerns applies to module boundaries in code; this pattern applies the same discipline to the schema of user intent. Each concern (what to notify about, how to deliver) gets its own storage axis, its own UI surface, its own mental model.

Seen in

  • systems/slack-notifications-2-0 — canonical instance. Before: desktop: everything | mentions | nothing (one enum, content × channel conflated). After: desktop: everything | mentions (content) + desktop_push_enabled: true | false (channel) — orthogonal.
Last updated · 470 distilled / 1,213 read