Skip to content

YELP 2026-04-22 Tier 3

Read original ↗

Yelp — How Yelp Keeps Server-Driven UI Consistent Across Four Platforms

Summary

Follow-up to Yelp's 2025-07-08 CHAOS backend deep-dive, this post unpacks Konbini — the auto-generated library family that bridges CHAOS (Yelp's SDUI framework) to Cookbook, Yelp's cross-platform design system. From a single JSON interface definition per component, a Jenkins pipeline generates four platform-specific libraries — Kotlin (Android), Swift (iOS), Python (backend serialisation), TypeScript (web) — that all stay in sync because they are generated from the same source file. Because Konbini is the single point of code generation, it is also where Yelp solves backward compatibility at the component level: every client bundles a spec file listing the component versions it supports, every request carries that spec context (e.g. android@23.0), and when a component introduces a breaking change (version 0.8 → 1.0) developers implement a migrate(V1) -> V0 method that backports the new model to the old shape for older clients. The post uses a running CookbookButton example to walk through JSON definition, generated code (Python backend, Kotlin client + renderer), wire format, design-token serialisation ({name, raw_value}), and the version-bump + migrate workflow. The post discloses no operational numbers but exposes the full architectural mechanism that makes Yelp CHAOS a Cookbook-native SDUI framework rather than a generic one.

Key takeaways

  1. Konbini is four auto-generated libraries from one JSON interface-definition file per component. Verbatim: "Konbini is a collection of automatically generated libraries that expose the Cookbook component interfaces to Python code and provide the serialization/deserialization logic for the clients. The libraries are generated from JSON interface definitions for each component." The four are componentinterfaces (Kotlin/Android), YLInterfaces (Swift/iOS), component_interfaces (Python backend), component-interfaces (React/TypeScript web). Canonical instance of patterns/single-json-spec-to-multi-platform-codegen. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  2. The JSON interface definition is the single source of truth. Every component is declared once in the component_interfaces repository as a JSON blob with name, version, description, owners, and parameters (each parameter has a type, description, and optional default). The verbatim Button example declares text, style (a cookbook.ButtonStyle enum), on_click (Nullable<Action>), size, and background_color (Color). Any change to this definition — adding a parameter, changing a type, renaming — regenerates all four libraries through a Jenkins pipeline: "Whenever a new commit is pushed to the component_interfaces repository, Jenkins pipelines automatically trigger the code generation process and publish new versions of the libraries." Canonical instance of concepts/single-source-interface-spec. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  3. Generated Python backend class handles serialisation; generated client classes handle deserialisation — but the two come from the same source so parameter names are guaranteed to match. The post shows side-by-side: CookbookButtonV0 in Python has a parameter_types dict mapping text → "String", style → "cookbook.ButtonStyle", on_click → "Nullable<Action>", etc. The generated Kotlin CookbookButtonInterfaceParams Moshi data class has the same field names via @Json(name = "text"), @Json(name = "on_click") etc. Verbatim: "Because both the Python and Kotlin code are generated from the same JSON source, the parameter names are guaranteed to stay in sync." (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  4. The wire format is consistent JSON regardless of client. Example (verbatim): {"name":"cookbook.Button", "version":"0.8","parameters":{"text":"Click me to go to Yelp","style":"primary","on_click":{"type":"open_url", "url":"https://yelp.com"},"size":"small","background_color": "COM_CAROUSELBUTTON_COLOR_BG_INLINE"}}. The client dispatches by name to the right deserialiser; by version to the right model class. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  5. Client-side rendering binds the deserialised interface to the actual Cookbook widget via a platform-specific extension method. Kotlin example: an @Composable fun CookbookButtonInterface.Render(...) method maps the deserialised parameters to a ButtonParams, then dispatches on the size enum to ButtonSmall / ButtonStandard / ButtonLarge. Konbini stops at the deserialised model; the render extension method is hand-written per platform and couples the model to the platform-native Cookbook widget family. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  6. Design tokens — colours, icons, gradients, shadows — are serialised as {name, raw_value} records carrying both the token name and the resolved value. Verbatim: "Each token is serialized as a simple string representing its name, and when exported, includes a raw_value field containing its actual value." Example: a ref-color-black-100 colour token serialises to {"name": "ref-color-black-100", "raw_value": {"a":1, "b":0, "g":0, "r":0}}. Tokens live in a separate repository curated by designers, published as JSON. Yelp calls them "tokens ... predefined, use-case-specific values, so they don't have to decide on these values from scratch for every component." Canonical instance of concepts/design-token-as-named-reference. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  7. Every breaking change in a component bumps the major version AND requires a migrate() method. Example (verbatim): changing Button's text from String to a custom FormattedText type bumps the component version from 0.8 to 1.0 ("This is a breaking change because older clients will receive a different type of object for the text field, which they don't know how to interpret."). Konbini generates an empty migrate() method on CookbookButtonV1 that developers implement to "backport an instance of CookbookButtonV1 to an instance of CookbookContainerV0." Yelp's example implementation: text=model.text.toString(). Default behaviour "is to throw an error signifying that backporting the model is not supported" — opting out of backward-compat is an explicit act, not an accident. Canonical instance of concepts/component-version-migrate-function and patterns/migrate-function-for-component-downgrade. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  8. Each client carries a spec file that lists all component versions it supports. Verbatim example: {"version":"23.0","interfaces":{"cookbook.Button":"1.0"}, "enumerations":{"cookbook.ButtonStyle":"0.1"}}. "The spec file is saved inside the Konbini repository and gets bundled into all of the four generated libraries. Whenever a new component is updated or created, it must be included in the spec so that the classes for it get generated. At the same time, when there's any change made to a spec, its version has to be bumped." Canonical instance of concepts/client-spec-version. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  9. Backward-compat resolution happens per-request via a Konbini context. Verbatim: "When a CHAOS view is requested by a client (in our example Android), a Konbini context object that includes the spec name and version is passed, something that looks like android@23.0. When the backend receives it, it will look at version 23.0 of the Android spec and see which version of the CookbookButton can be sent back to the client without breaking it." If backend tries to instantiate CookbookButtonV1 but the client supports only V0, the migrate() method is invoked to produce a V0. Canonical instance of patterns/spec-version-negotiation-for-backward-compat. (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

  10. Konbini closes the loop on SDUI cross-platform consistency by removing all hand-written serialisation/deserialisation code. Pre-Konbini anti-examples from the post: two teams shipped different backend representations for what should have been the same Button (SDUIButton{label, button_type, size="small"} vs ServerButton{text, style, display_size="sm"}) — different field names, different values, different action shapes. Post-Konbini, all backends instantiate the same generated CookbookButton class whose output is bit-compatible with the clients' generated deserialisers. Verbatim: "Both solutions were trying to render the same button component, but their backend representations were completely different ... This created a maintenance burden and made it difficult to ensure a unified user experience." (Source: sources/2026-04-22-yelp-how-yelp-keeps-server-driven-ui-consistent-across-four-platforms)

Architectural diagram (post-facto reconstruction)

                   component_interfaces
                   (Git repository — one JSON per component)
                              │  push → Jenkins pipeline
          ┌───────────────────────────────────────────┐
          │          Konbini code generation          │
          └───────────────────────────────────────────┘
                 │         │         │         │
                 ▼         ▼         ▼         ▼
          componentinterfaces       component_interfaces
          (Kotlin / Android)        (Python / backend)
                 │                         │
                 ▼                         ▼
          Android app                 CHAOS backend
          (deserialise JSON)          (instantiate +
                 │                    serialise JSON)
                 ▲                         │
                 │  spec: android@23.0     │
                 │                         ▼
                 └── wire format ───── CHAOS Configuration
                         JSON:        {components: [...],
                    {name, version,    actions: [...]}
                     parameters: {...}}

          YLInterfaces              component-interfaces
          (Swift / iOS)             (TypeScript / React web)
          ──────                     ──────
          (same shape)              (same shape)


                   design tokens
                   (separate repo curated by designers)
                   tokens.json  →  bundled into libs →  {name, raw_value}

Reference: generated code samples from the post

Input — JSON interface definition

{
  "name": "cookbook.Button",
  "version": "0.8",
  "description": "A customizable Cookbook button.",
  "owners": ["Design Systems <design-systems@yelp.com>"],
  "parameters": {
    "text":   { "type": "String",
                "description": "The text to show in the button." },
    "style":  { "type": "cookbook.ButtonStyle",
                "description": "The style that corresponds to ..." },
    "on_click":{"type": "Nullable<Action>", "default": null,
                "description": "An action to be executed when the button is clicked." },
    "size":   { "type": "cookbook.ButtonSize", "default": "standard",
                "description": "A predetermined value for how large the button is." },
    "background_color": { "type": "Color",
                          "description": "Background color of the button." }
  }
}

Output — Python (backend) serialiser class

class CookbookButtonV0:
    parameters: CookbookButtonParametersV0

    def __init__(self, text, style, on_click=None,
                 size=CookbookButtonSize("standard"),
                 background_color):
        self.parameters = CookbookButtonParametersV0(
            text=text, style=style, on_click=on_click,
            size=size, background_color=background_color,
        )
        self.parameter_types = {
            "text": "String",
            "style": "cookbook.ButtonStyle",
            "on_click": "Nullable<Action>",
            "size": "cookbook.ButtonSize",
            "background_color": "Color",
        }
        self.version = "0.8"

CookbookButton = CookbookButtonV0

Output — Kotlin (Android) deserialiser data class

@JsonClass(generateAdapter = true)
data class CookbookButtonInterface(
    val parameters: CookbookButtonInterfaceParams,
) : InterfaceModel {
    override val name: String get() = specName
    companion object {
        const val specName: String = "cookbook.Button"
        const val specVersion: String = "0.8"
    }
}

@JsonClass(generateAdapter = true)
data class CookbookButtonInterfaceParams(
    @Json(name = "text")              val text: String,
    @Json(name = "style")             val style: CookbookButtonStyle,
    @Json(name = "on_click")          val onClick: ActionModel? = null,
    @Json(name = "size")              val size: CookbookButtonSize = CookbookButtonSize.standard,
    @Json(name = "background_color")  val backgroundColor: ColorToken,
) : ParamsModel { companion object { /* specName, specVersion */ } }

Output — hand-written Kotlin render extension (NOT generated)

@Composable
fun CookbookButtonInterface.Render(
    renderer: KonbiniComposeRenderer,
    onError: (RenderError) -> Unit,
    modifier: Modifier = Modifier,
) {
    with(parameters) {
        val buttonParams = ButtonParams(
            modifier = modifier,
            text = text,
            style = style,
            color = backgroundColor,
            onClick = onClick?.let { renderer.handler(action = it, onError = onError) } ?: {},
        )
        when (size) {
            small    -> ButtonSmall(buttonParams)
            standard -> ButtonStandard(buttonParams)
            large    -> ButtonLarge(buttonParams)
        }
    }
}

Wire format (JSON on the wire, same for every platform)

{
  "name": "cookbook.Button",
  "version": "0.8",
  "parameters": {
    "text": "Click me to go to Yelp",
    "style": "primary",
    "on_click": {"type": "open_url", "url": "https://yelp.com"},
    "size": "small",
    "background_color": "COM_CAROUSELBUTTON_COLOR_BG_INLINE"
  }
}

Token serialisation ({name, raw_value})

def serialize_color(value):
    return {
        "name": value,
        "raw_value": CookbookResources.instance().colors[value]["default"],
    }
"color": {
  "name": "ref-color-black-100",
  "raw_value": {"a": 1, "b": 0, "g": 0, "r": 0}
}

Spec file (per platform; bundled in the generated lib)

{
  "version": "23.0",
  "interfaces":    { "cookbook.Button": "1.0" },
  "enumerations":  { "cookbook.ButtonStyle": "0.1" }
}

Breaking-change workflow — migrate() method

def migrate(model: CookbookButtonV1) -> CookbookButtonV0:
    """
    Backport CookbookButtonV1 to CookbookButtonV0 so views using
    this component stay compatible with older clients.
    Default behaviour: raise NotSupportedError. Override per field.
    """
    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,
    )

Operational details

  • Source of truth: component_interfaces Git repo; one JSON file per component; Jenkins triggers codegen on every push.
  • Four generated libraries (same interface contract, different language bindings):
  • componentinterfaces — Kotlin (Android, Moshi-based JSON deserialisation).
  • YLInterfaces — Swift (iOS).
  • component_interfaces — Python (backend, serialisation to JSON).
  • component-interfaces — TypeScript (React, web).
  • Element vocabulary coupling: backend CookbookButton class has the same field names as the generated Kotlin CookbookButtonInterfaceParams — guaranteed by shared codegen source.
  • Rendering coupling: a hand-written per-platform render extension (e.g. CookbookButtonInterface.Render) maps the deserialised interface to the platform-native Cookbook widget family (e.g. ButtonSmall, ButtonStandard, ButtonLarge for Android Compose).
  • Design tokens: curated in a separate repo by designers; exported as {name, raw_value} records — token name is stable across platforms, raw_value is platform-resolvable.
  • Spec file: one per platform (android@23.0, ios@<N>, etc.); lists the versions of every interface + enumeration that platform supports; bundled into the generated library.
  • Backward-compat negotiation: every CHAOS request carries a spec_name@spec_version context; backend picks the highest component version that the client's spec allows; if backend constructs a newer version than the client supports, migrate() is invoked to downgrade.
  • Versioning policy: any breaking change to a component (type changed, parameter removed, etc.) triggers a major bump (0.8 → 1.0). Non-breaking additions are minor bumps within the same major. Any spec change bumps the spec version.

Caveats / gotchas

  • No operational numbers disclosed. No latencies, no RPS, no number of components, no build-times, no library-publish frequency. This is an architecture walkthrough, not a retrospective.
  • Render extensions are hand-written and per-platform. Konbini stops at the deserialised model — the mapping from the model to the platform-native Cookbook widget is hand-maintained. A render-extension regression is still a possible failure mode even though serialisation is codegen.
  • migrate() correctness is developer responsibility. The generated default throws. A bad migration silently ships the wrong backport to older clients; Yelp's post doesn't disclose testing discipline around migrate functions.
  • Cross-major-version migrate chains are not discussed. The post shows a single V1 → V0 migrate. If a component reaches V2.0 while clients still run specs pointing at V0, do migrate methods chain (V2 → V1 → V0) or does each pair need its own pairwise migrate? Not stated.
  • Action types and enum types use their own versioning. The spec file example shows enumerations as a separate map from interfaces (cookbook.ButtonStyle: "0.1") — enum versioning is orthogonal to interface versioning, but the post doesn't explicitly cover enum migration.
  • Token repo governance is opaque. "These tokens are maintained in a separate repository curated by our designers and are provided in JSON format." Who can deprecate a token, what happens when a token a component depends on is renamed, and how that propagates to older-version clients is not discussed.
  • Predecessor 2024-03 CHAOS introduction post is still not ingested — the two ingested Yelp CHAOS posts (this one
  • 2025-07-08 backend) cover only the 2024-03 companion's successor material. The 2024-03 post is explicitly referenced as "If you've read our earlier post ...".

Source

Last updated · 550 distilled / 1,221 read