Skip to main content

Viaduct: When Airbnb Said 'Hold My GraphQL' to the Entire Industry

· 9 min read
GraphQL Guy

Viaduct Architecture

You know how most companies build a GraphQL API, hit some scaling issues, and then write a blog post about it? Airbnb took a different approach: they built an entire data-oriented service mesh, ran it for five years at massive scale, and then open-sourced it. Meet Viaduct - GraphQL's ambitious cousin who went to architecture school.

What Even Is Viaduct?

Let's start with the basics. According to Airbnb's engineering blog, Viaduct is:

"A GraphQL-based system that provides a unified interface for accessing and interacting with any data source."

But that undersells it dramatically. Viaduct is really three things:

  1. A GraphQL execution engine (built on graphql-java)
  2. A serverless platform for hosting business logic
  3. A data-oriented service mesh that connects 130+ teams

Since its 2020 announcement, traffic through Viaduct has grown 8x, with teams contributing 1.5+ million lines of hosted code. This isn't a proof-of-concept. This is battle-tested infrastructure.

┌─────────────────────────────────────────────────────────────────────┐
│ VIADUCT AT A GLANCE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ • Built on graphql-java with Kotlin runtime │
│ • Runs on Spring Boot │
│ • 130+ teams contributing code │
│ • 1.5M+ lines of hosted business logic │
│ • 75% of requests are internal (service-to-service) │
│ • 8x traffic growth since 2020 │
│ │
│ Not just a GraphQL layer - a platform for hosting logic │
│ │
└─────────────────────────────────────────────────────────────────────┘

The Problem Viaduct Solves

Before Viaduct, Airbnb had the typical microservices problem:

┌─────────────────────────────────────────────────────────────────────┐
│ THE BEFORE TIMES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Mobile App │
│ │ │
│ ├──▶ User Service ──▶ Database │
│ ├──▶ Booking Service ──▶ Database ──▶ User Service (again!) │
│ ├──▶ Search Service ──▶ Database ──▶ Listing Service │
│ ├──▶ Payment Service ──▶ ... │
│ └──▶ (47 more services) │
│ │
│ Problems: │
│ ❌ Clients need to know about every service │
│ ❌ Services call each other chaotically │
│ ❌ No unified data model │
│ ❌ Observability nightmare │
│ │
└─────────────────────────────────────────────────────────────────────┘

Viaduct's solution: put a single, integrated GraphQL schema in front of everything.

┌─────────────────────────────────────────────────────────────────────┐
│ THE VIADUCT WAY │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Mobile App │
│ │ │
│ └──▶ Viaduct (single GraphQL endpoint) │
│ │ │
│ ├──▶ User Tenant Module │
│ ├──▶ Booking Tenant Module │
│ ├──▶ Search Tenant Module │
│ └──▶ (all other modules) │
│ │ │
│ └──▶ Downstream services / databases │
│ │
│ Benefits: │
│ ✅ One schema, one endpoint │
│ ✅ Teams own their piece of the graph │
│ ✅ Built-in batching, caching, observability │
│ ✅ Logic composes via GraphQL fragments │
│ │
└─────────────────────────────────────────────────────────────────────┘

The Architecture: Three Layers

Viaduct Modern (their latest iteration) has a clean three-layer design:

Layer 1: GraphQL Execution Engine

The engine is dynamically typed - it works with GraphQL values as maps from field name to value. This is the raw graphql-java layer with Airbnb's optimizations.

Layer 2: Tenant API

The tenant API is statically typed. Viaduct generates Kotlin classes for every GraphQL type in the schema. This is what developers interact with.

Layer 3: Application Code

Your business logic. This is where the magic happens.

┌─────────────────────────────────────────────────────────────────────┐
│ VIADUCT LAYERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Layer 3: Application Code (Your Business Logic) │ │
│ │ • Node resolvers, Field resolvers │ │
│ │ • Hosted by teams in "tenant modules" │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Layer 2: Tenant API (Generated Kotlin Classes) │ │
│ │ • Strongly typed │ │
│ │ • Schema-first: write GraphQL, get Kotlin │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Layer 1: GraphQL Execution Engine (graphql-java) │ │
│ │ • Dynamically typed │ │
│ │ • Parsing, validation, execution │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ This separation lets engine and API evolve independently │
│ │
└─────────────────────────────────────────────────────────────────────┘

The Tenant Module Model

Here's where Viaduct gets interesting. Instead of one giant GraphQL service, you have tenant modules:

A tenant module is a unit of schema together with the code that implements that schema, and crucially, is owned by a single team.

Each team owns their slice:

// User Tenant Module
// Owned by: Team Identity

@NodeResolver
class UserResolver {
suspend fun resolve(id: GlobalId): User? {
return userService.findById(id.localId)
}
}

@FieldResolver
class UserFieldsResolver {
suspend fun User.displayName(): String {
return "${this.firstName} ${this.lastName}"
}

suspend fun User.bookings(first: Int = 10): List<Booking> {
// This calls INTO the Booking tenant module
// via GraphQL fragment - not direct code dependency!
return viaduct.query("""
fragment on User {
bookings(first: $first) {
id
checkIn
checkOut
}
}
""", mapOf("first" to first))
}
}

The key insight: modules compose via GraphQL, not code. The User module doesn't import the Booking module. It queries it through GraphQL fragments.

Re-entrancy: The Secret Sauce

At the heart of Viaduct is re-entrancy:

Logic hosted on Viaduct composes with other logic hosted on Viaduct by issuing GraphQL fragments and queries.

This is unusual. Most GraphQL frameworks have resolvers call services directly. Viaduct has resolvers call... GraphQL.

// Traditional approach (what Spring for GraphQL does)
@SchemaMapping
fun bookings(user: User): List<Booking> {
// Direct code dependency
return bookingService.findByUserId(user.id)
}

// Viaduct approach
@FieldResolver
suspend fun User.bookings(): List<Booking> {
// GraphQL composition
return viaduct.query("""
query GetBookings($userId: ID!) {
bookingsForUser(userId: $userId) {
id
listing { name }
checkIn
}
}
""", mapOf("userId" to this.id))
}

Why does this matter?

  1. No code dependencies between modules - Teams stay decoupled
  2. Automatic batching - Viaduct batches these internal queries
  3. Consistent observability - All calls go through the graph
  4. Modularity at scale - 130+ teams, no monolith hazards

Batching and Caching: Built-In

Viaduct doesn't just support DataLoader - it makes batching fundamental:

┌─────────────────────────────────────────────────────────────────────┐
│ AUTOMATIC BATCHING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Query: │
│ { │
│ user(id: "1") { bookings { listing { name } } } │
│ user(id: "2") { bookings { listing { name } } } │
│ user(id: "3") { bookings { listing { name } } } │
│ } │
│ │
│ Without Viaduct: │
│ • 3 user fetches │
│ • N booking fetches │
│ • M listing fetches │
│ • Total: 3 + N + M round trips │
│ │
│ With Viaduct: │
│ • 1 batched user fetch │
│ • 1 batched booking fetch │
│ • 1 batched listing fetch │
│ • Total: 3 round trips (regardless of N and M) │
│ │
└─────────────────────────────────────────────────────────────────────┘

Plus: intra-request caching, type-safe Global IDs, soft dependencies, and short-circuiting for reliability.

Viaduct vs. Spring for GraphQL

Now for the comparison everyone's waiting for. How does Viaduct differ from Spring for GraphQL?

AspectSpring for GraphQLViaduct
PhilosophyLibrary for building GraphQL APIsPlatform for hosting business logic
OwnershipYou own the full stackViaduct owns execution, you own modules
SchemaPer-applicationOne central schema, many contributors
Code generationOptional (use records/classes)Core feature, generates Kotlin
CompositionDirect code callsGraphQL fragment composition
Batching@BatchMapping, DataLoaderBuilt into the platform
Multi-tenancyYou implement itFirst-class concept
TargetAny Spring teamLarge orgs with many teams

When to Choose Spring for GraphQL

You're a single team building a GraphQL API ✅ You want full control over your stack ✅ Your schema is self-contained (one service, one schema) ✅ You prefer Java (Spring for GraphQL is Java-first) ✅ Simpler is better for your use case

Spring for GraphQL is excellent for:

@Controller
public class MovieController {

@QueryMapping
public Movie movie(@Argument Long id) {
return movieService.findById(id);
}

@SchemaMapping
public Director director(Movie movie) {
return directorService.findById(movie.getDirectorId());
}

@BatchMapping
public Map<Movie, List<Review>> reviews(List<Movie> movies) {
return reviewService.findByMovies(movies);
}
}

Clear, simple, powerful. You own everything.

When to Choose Viaduct

You have many teams (10+) contributing to one graph ✅ You want enforced modularity between teams ✅ You need a serverless model (host logic, not services) ✅ You're Kotlin-first (Viaduct leverages coroutines heavily) ✅ Observability is critical (field-level attribution)

Viaduct excels when:

// Team A: Identity
@NodeResolver
class UserResolver {
suspend fun resolve(id: GlobalId): User? = userService.find(id)
}

// Team B: Bookings (no code dependency on Team A)
@FieldResolver
class BookingUserResolver {
suspend fun Booking.guest(): User {
return viaduct.resolve(this.guestId) // Goes through the graph
}
}

// Team C: Reviews (no code dependency on A or B)
@FieldResolver
class ReviewResolver {
suspend fun Listing.reviews(): List<Review> {
return reviewService.findByListingId(this.id)
}
}

130 teams. One graph. No merge conflicts. No monolith.

The Technical Differences

Coroutines vs. Threads

Viaduct uses Kotlin coroutines heavily:

// Viaduct: Coroutines are first-class
@FieldResolver
suspend fun User.bookings(): List<Booking> {
// Suspends, doesn't block threads
return bookingService.findByUser(this.id)
}

Spring for GraphQL supports reactive types but defaults to thread-per-request:

// Spring for GraphQL: Blocking by default
@SchemaMapping
public List<Booking> bookings(User user) {
// Blocks a thread
return bookingService.findByUser(user.getId());
}

// Or reactive
@SchemaMapping
public Flux<Booking> bookings(User user) {
return bookingService.findByUserReactive(user.getId());
}

Schema Ownership

Spring for GraphQL: Each application owns its schema.

App A: schema.graphqls (owns types A, B, C)
App B: schema.graphqls (owns types D, E, F)

Viaduct: One central schema, many contributors.

Central Schema:
├── User (owned by Identity team)
├── Booking (owned by Reservations team)
├── Listing (owned by Homes team)
├── Review (owned by Trust team)
└── ... (130+ teams contribute)

Build System Integration

Viaduct invests heavily in developer experience:

We've made investments including direct-to-bytecode code generation that bypasses lengthy compilation of generated code.

Spring for GraphQL relies on standard Java/Kotlin compilation. Fast, but not optimized for massive codebases.

Getting Started with Viaduct

Viaduct is open source on GitHub. Here's a taste:

// Define your schema
// src/main/resources/graphql/movie.graphqls

type Movie @key(fields: "id") {
id: ID!
title: String!
releaseYear: Int!
director: Director!
}

type Director @key(fields: "id") {
id: ID!
name: String!
}

type Query {
movie(id: ID!): Movie
movies: [Movie!]!
}
// Implement resolvers
@NodeResolver
class MovieResolver(private val movieService: MovieService) {
suspend fun resolve(id: GlobalId): Movie? {
return movieService.findById(id.localId)
}
}

@FieldResolver
class MovieFieldResolver(private val directorService: DirectorService) {
suspend fun Movie.director(): Director {
return directorService.findById(this.directorId)
?: throw NotFoundException("Director not found")
}
}

@QueryResolver
class MovieQueryResolver(private val movieService: MovieService) {
suspend fun movie(id: ID): Movie? = movieService.findById(id)
suspend fun movies(): List<Movie> = movieService.findAll()
}

The code generation creates typed Kotlin classes from your schema, and the engine handles batching, caching, and observability.

Should You Use Viaduct?

Here's my honest take:

Yes, if:

  • You're at Airbnb scale (or heading there)
  • You have 10+ teams that need to contribute to one graph
  • You want platform-level features (batching, caching, observability) without building them
  • You're comfortable with Kotlin
  • You value enforced modularity over flexibility

No, if:

  • You're a single team or small org
  • You want to own your full stack
  • You prefer Java over Kotlin
  • Your GraphQL needs are straightforward
  • You don't want to adopt a new platform

Maybe, if:

  • You're growing fast and anticipate multi-team needs
  • You're evaluating federation alternatives
  • You want to learn from Airbnb's architecture decisions

The Bigger Picture

Viaduct represents a different philosophy than most GraphQL frameworks. It's not "here's a library, build your API." It's "here's a platform, host your logic."

Spring for GraphQL says: "You're building a GraphQL server." Viaduct says: "You're contributing to a graph."

Neither is wrong. They solve different problems.

If you're building a GraphQL API for your application, Spring for GraphQL (or Apollo Server, or Strawberry, or whatever) is probably right.

If you're building a unified data layer for a large organization with many teams, Viaduct is worth a serious look.

The fact that Airbnb open-sourced it after five years of production use - with 130+ teams and 1.5M lines of code - suggests they're confident it solves real problems.

At minimum, read their engineering blog posts. Even if you never use Viaduct, the architectural patterns are worth understanding.


This blog post was composed via GraphQL fragments from multiple tenant modules in my brain. Re-entrancy is a hell of a drug.

Sources