Tinder API Style Guide: Part 2

Nishant Mittal
Principal Software Engineer, Backend
Ryan Trontz
Tech Lead, Backend Platform
At scale, fragmented schemas create fragile systems. We standardized Protobuf across Tinder, pairing it with enforcement and tooling that make schema changes predictable and reliable.

API Style Guide Advisory Group: Vijaya Vangapandu, Devin Thomson and Greg Giacovelli

In Part 1 of this series, we explored the challenges Tinder faces in building scalable APIs, focusing on URI design, HTTP methods, request headers and request/response body standards.

But good API design isn’t just about endpoints. It also depends on the schemas that define the data moving through them.

At Tinder’s scale, hundreds of services, clients, and event pipelines depend on shared data models. For years, those models evolved at different paces in different places: JSON contracts in some APIs, Protobuf definitions scattered across repositories, and ad-hoc solutions. Over time, that fragmentation made it hard to answer simple questions: Where does this schema live? Which version is correct? And what happens if someone changes it?

This post focuses on how we tackled that complex problem. Here, we walk through how Tinder standardized Protobuf as the single source of truth across REST, gRPC, and our Event Bus (Tinders’ event processing pipeline), and how we built the governance and tooling needed to make those schemas reliable at scale.

What was the core problem?

Before we standardized, Protobuf at Tinder was fragmented across three worlds: TinderApp/proto for client-server contracts, dozens of tinder backend repos, and improvised definitions for our Event Bus and internal services. No one was quite sure where a given schema should live or which version to rely on, especially for Node.js services and events/messages/models that didn’t map cleanly to a single service repo.

On top of that, the JavaScript toolchain behind TinderApp/proto was effectively frozen on google-protobuf and ts-protoc-gen, which meant we couldn’t even use optional in many places. Different teams copied patterns from whatever examples they found: inconsistent package names, mixed casing styles, and diverging rules for field presence. The result was exactly what you’d expect at our size: brittle clients, defensive parsing code, and production bugs that came down to “this field changed type” or “this enum grew a new value.”

What did we try (and fail with)?

Before Protobuf became the contract of record, we tried to lean on OpenAPI/Swagger for REST contracts. In practice, our implementation was incomplete and sometimes inverted defaults, which meant repos claiming “we use OpenAPI” behaved differently from the actual spec and confused everyone who trusted the docs.

We also experimented with “Wrapper Response” style envelopes to normalize responses at the JSON layer, but over time that envelope became a dumping ground rather than a clear contract. It didn’t address the deeper problem: we still lacked a consistent schema that worked across REST, gRPC, and Event Bus.

On the Protobuf side, we let each tinder-* repo publish its own protos independently. That seemed flexible, but it made discoverability and governance nearly impossible with dozens of CI pipelines, each with slightly different rules, and no easy way to prevent breaking changes.

How did we even approach the problem?

We started by writing down the pain in an RFC: where should internal protos live, where should Event Bus schemas live, and how do we stop accidentally re‑solving these questions for every new service? That RFC proposed a single home, tinder-proto, with clear separation between external (client‑facing) and internal (backend & Event Bus) protos, and explicit artifact naming and package conventions for each.

In parallel, the API Standardization WG scoped the broader API story: when do we use REST vs gRPC, how do we map REST responses to Protobuf, and how do we use schema‑driven generation for documentation and SDKs. The Protobuf style guide became the schema layer of that strategy, aligned with the REST Style Guide, so that JSON and Protobuf told the same story.

How did we solve the problem?

We treated Protobuf governance the same way we treated the REST Style Guide: as a product, not a wiki page. The API Standardization Working Group was chartered to close gaps between our current APIs and industry standards, with a specific mandate around schema standardization and Protobuf usage.

From there, we did three big things:

  1. Centralized the schemas into shared mono‑repos (tinder-api-proto for client–server, tinder-proto for internal) so there was exactly one place to define and discover API models
  2. Codified the rules in a dedicated Tinder Protocol Buffer Style Guide, layered on top of Google’s and Buf’s official guidance
  3. Automated the enforcement with Buf linting, breaking‑change checks, and multi‑language codegen in CI so that “standards” became build‑breaking, not aspirational

What were the technical challenges in building the solution?

A few of the harder parts:

  • Live traffic and legacy clients. Many protos were already in production; we couldn’t just “fix” them to match the new style without breaking wire compatibility or forcing mass client migrations.
  • Different usage patterns. Event bus messages, internal protos, and public API models don’t want the exact same rules. For example, Dynamo stream adapters still rely on wrapper types and camelCase fields due to framework constraints, so we had to define documented carve‑outs rather than pretend one set of rules fits all.
  • Multi‑platform tooling. iOS needed Bazel‑built artifacts, web needed TypeScript definitions, and backend wanted Gradle‑driven Java/Kotlin models—all generated from the same sources without every team learning protoc internals.
  • Culture change. Moving from “whatever the repo does” to “the style guide is the law, enforced by CI” required agreement from service teams, client teams, and platform engineers, not just a doc drop.

What did we build?

The future of Tinder! But more concretely, we created:

  • A centralized proto mono‑repo (tinder-api-proto for public APIs, evolving toward tinder-proto for both external and internal models) with consistent artifact naming (<project>-proto-models, <project>-api-proto).
  • A Tinder Protocol Buffer Style Guide that sits alongside the REST Style Guide and calls out our conventions for packages, enums, field naming, presence, JSON mapping, errors, and well‑known types.
  • Buf-based enforcement for style, naming, and breaking‑change detection, wired into CI jobs like runAffectedBufLint and runAffectedBreakingChange so regressions fail fast.
  • A versioned publishing pipeline that generates Java/Kotlin, JS/TS, and iOS artifacts from the same schemas and pushes them to our artifact repositories.

At a high level, our Protobuf standards rest on three pillars:

  1. Iterative development philosophy: We design messages to start simple and grow over time, rather than guessing every field up front. We assume fields will be added, deprecated, and replaced as the product evolves.
  2. Compatibility guarantees: We treat backward and forward compatibility as non‑negotiable: old clients must be able to read new messages, and new clients must handle older payloads without crashing.
  3. JSON integration: Every Protobuf message must serialize cleanly to JSON with explicit json_names so the same schema can back both REST and gRPC, and remain debuggable in logs and tools.

Under those pillars, the style guide defines the details: proto3 syntax everywhere, hierarchical message design, strict field lifecycle rules, and consistent naming across filenames, packages, messages, fields, and enums.

How does it work?

Versioning and evolution

All files use proto3, and we strongly prefer incremental evolution over “V2” style message forks. You don’t introduce TextElementV2; you add a new field to TextElement, mark old fields deprecated = true with clear comments, and then reserve their field numbers when you finally remove them.

// ✅ Correct approach
message TextElement {
  string body = 1;
  string hex_color = 2;  // Added incrementally
}

// ❌ Avoid this
message TextElementV2 {
  string body = 1;
  string hex_color = 2;
}

Structure and naming

  • Packages start with tinder (or tinder.api / tinder-protointernal) and mirror directory layout.
  • Java options (java_package, java_multiple_files, java_outer_classname) are mandatory for predictable codegen.
option java_multiple_files = true;




option java_outer_classname = "ConversationsApiV1";
option java_package = "com.tinder.api.conversations.v1";
  • Messages and enums use PascalCase, fields use snake_case, and enum values are ALL_CAPS_SNAKE_CASE, often prefixed with the enum name to avoid collisions.
Components and Case Style

Enums

Every enum MUST include an UNSPECIFIED value at position 0

enum ConversationType {
  CONVERSATION_TYPE_UNSPECIFIED = 0;  // Required default
  CONVERSATION_TYPE_DM = 1;
  CONVERSATION_TYPE_GROUP = 2;
}


Field presence

We use optional for scalar presence, avoid required, and only use wrapper types in legacy contexts where they’re already established.

Errors

Errors are modeled as dedicated messages (e.g., ErrorProto) aligned with REST error guidance, instead of "oneof data-or-error” wrappers in the main response type.

Well-known types

We lean on well‑known types like Duration where they add value, but prefer ISO‑8601 strings over google.protobuf.Timestamp to keep the ecosystem interoperable.

Automation

On every change, CI:

  • Runs Buf lint to catch style and naming issues (including casing) before review
  • Runs breaking‑change detection to block wire‑incompatible edits once a package crosses 1.0.0
  • Builds and publishes updated artifacts for all platforms, so the generated code lines up with the monorepo state
# BufLint Job:
./gradlew clean runAffectedBufLint

# Breaking Change Detection:
./gradlew clean runAffectedBreakingChange

# Unit Testing:
./gradlew clean runAffectedUnitTests

What’s the impact?

The tangible effects have shown up in three places:

  • Developer productivity. New services follow a known pattern: pick a package, run the proto project generator, follow the style guide. Engineers spend less time bikeshedding naming and more time on behavior.
  • System reliability. Breaking changes are caught at PR time instead of in production. Buf and CI now act as a guardrail for API evolution.
  • Cross‑platform consistency. Android, iOS, web, and backend all now compile from the same schemas, making it much harder for platform‑specific drift to creep in.

In practical terms, this means fewer “mystery crashes” on old app versions, simpler rollbacks, and more predictable upgrades when we add or deprecate fields.

What’s great about our new life

Today, if you’re adding a new API:

  • There’s one repo to add your proto to.
  • There’s one style guide that tells you how to name packages, fields, enums, and errors.
  • There’s one set of CI checks that will yell at you if you break compatibility or drift from the conventions.

Client teams no longer have to reverse‑engineer response shapes by sniffing traffic; they can rely on generated models and descriptors. Backend teams don’t have to remember ten different ways we modeled “user” or “photo” in different services. And the API Standardization WG can focus on genuinely hard design questions instead of re‑litigating casing or enum defaults on every PR.

What’s still not perfect yet

Like most engineering stories, this one includes a little archaeology as things evolve. We’re still living with some pre‑standards history:

  • Legacy package names and schemas that don’t fully align with the new guide, but are too risky to “fix” without a migration plan.
  • Carve‑outs for systems like event bus and Dynamo stream consumers, where framework constraints force wrappers or camelCase even though the default guidance prefers optional and snake_case.
  • Internal vs external duplication. The tinder-proto RFC calls out that keeping a hard separation between internal and external protos can mean some model duplication (e.g., internal vs external User), which isn’t fully resolved yet.

And culturally, we’re still migrating teams off of older patterns (like V2Response wrappers or JSON‑only models) toward Protobuf‑first contracts everywhere.

What’s next

The roadmap is less about inventing new rules and more about finishing the migration and tightening the loop:

  • Complete the move of “homeless” protos and legacy TinderApp/proto definitions into tinder-proto, under clear internal/external boundaries.
  • Expand descriptor publishing and tooling so debugging Protobuf over HTTP (e.g., via Charles) is as easy as inspecting JSON today.
  • Continue iterating on the Protocol Buffer Style Guide as we learn from real migrations, especially around enums, error modeling, and JSON projections.
  • Tighten CI rules (for example, around package versioning and v1 packages) as more projects reach 1.0 and beyond.

The end state is simple to describe, even if it took us years to get here: every structured payload at Tinder, whether it flows through  REST, gRPC, or Event Bus, moves through a small set of well‑governed Protobuf contracts, growing safely over time. The goal is not perfection, but predictability: a system where engineers can work, evolve, and build future systems knowing that foundation is secure.

Tags for this post:
No items found.