Skip Navigation
Trulia Logo

Trulia Blog

Creating a Unified Interface for Mobile Clients with GraphQL

This post is a continuation of our series about paying down technical debt and re-architecting our platform. You can read the introductory post here: Paying Technical Debt to Focus on the Future.

Like other engineering groups at Trulia, the iOS team understood the challenges associated with our monolithic architecture.  We dealt with performance issues, delayed releases, and a general lack of parity between iOS features and those on other platforms. We were often negatively impacted by upstream decisions and forced to come up with workarounds and stopgaps that only compounded many of the challenges we were facing. Trulia’s decision to move from a monolithic to a microservices architecture created an opportunity for the mobile team to permanently address many of our challenges, as well as pay down some of our technical debt with the implementation of GraphQL.

Technical Debt: Mobile API

The problems created by interacting with the monolith can be traced back to the creation of our mobile applications.  This includes our greatest challenge — the data we needed on mobile clients was often scattered across multiple services, connected only by complex relationships. This inefficient configuration generated multiple requests, returned data that we did not need, and wasted resources.

We addressed that issue by creating a facade REST interface. The facade handled all the complications of dealing with the various pieces of the monolithic architecture and serialized data from various sources into a consistent structure, easily consumed by mobile clients.  

While this simplified our interactions with the monolith, it created other challenges. Any new APIs that the mobile applications required access to would need to be implemented by the facade as well, resulting in longer overall development times.  Changes in upstream API interfaces also needed to be applied to the facade, requiring double maintenance. Roadmaps for teams responsible for various APIs and the facade were often unaligned, resulting in a disparity between web and mobile applications

As we considered the move to a microservice architecture, we wanted to understand how we could retain the benefits of a facade, while addressing the needs of every client.

The Solution: GraphQL

We decided to create a unified interface with a GraphQL based API that provides only the necessary data to individual clients in a consistent format. The use of a single interface encouraged all stakeholders to participate and contribute to the design of the overall schema and domain model, ensuring each team’s needs are met.  

An example of this collaboration occurred early on in the schema design for Trulia Neighborhoods. We realized that individual clients had different preferences for image compression types — iOS supports HEIF compression,  Android prefers WebP, and mobile web prefers png. GraphQL allowed us to add parameters so each client can pass in its preferred compression formats for remote image resources.

Additionally, unifying under GraphQL provided the opportunity to move business logic, commonly implemented on each client, to the resolvers within the GraphQL layer.  This removed the requirement of clients to update their code when business rules dictating how to display data changed.

Integration

Implementing GraphQL proved to be a straightforward process with Apollo provided SDKs and tools for creating and handling requests to the GraphQL server.  The toolset allowed us to write our queries in GraphQL syntax and auto-generate the code necessary to perform the query in each of our unique clients. While we are unified by single query language iOS queries are generated in Swift, Android queries in Java/Kotlin, and web queries in TypeScript.

Android

public final class LocationQuery implements Query {

	private final LocationQuery.Variables variables;

	public LocationQuery(@Nonnull Input id) { ... }

	@Override
	public LocationQuery.Data wrapData(LocationQuery.Data data) { ... }

	@Override
	public LocationQuery.Variables variables() { ... }

	public static final class Builder { ... }

	public static final class Variables extends Operation.Variables { ... }

	public static class Data implements Operation.Data { ... }

	public static class Location {

	  final @Nonnull String __typename;

	  final @Nullable Long id;

	  final @Nullable String name;

	  ...

	}
}

iOS

public final class GetLocationQuery: GraphQLQuery {
  public let operationDefinition =
    "query getSurrounding($id: Int) {n  Location(id: $id) {n    __typenamen    idn    namen  }n}"

  public var id: Int?

  public init(id: Int? = nil) {
    self.id = id
  }
  ...
  public struct Data: GraphQLSelectionSet {
    ...

    public var location: Location? { ... }

    public struct Location: GraphQLSelectionSet {
      ...

      public var id: Int? { ... }

      public var name: String? { ... }
    }
  }
}

Challenges

Despite the ease of integrating GraphQL, we still faced some technical challenges.  First, we needed to find a way to simultaneously support GraphQL for newer features, as well as legacy REST endpoints. We decided on a hybrid approach that took advantage of the fact that GraphQL requests are handled through the Apollo Client.

One of the components of the Apollo Client is a network transport which is responsible for serializing and performing network requests and passing the raw response data to a data deserializer.  This pattern already existed in our current networking stack, which automatically handled the injection of certain domain-specific headers needed for our application.

Fortunately, the Apollo client uses a protocol-oriented pattern and allows developers to inject their own network transport.  We created a custom network transport that would conform to Apollo’s network transport protocol, while utilizing our existing networking layer, thus eliminating the necessity to duplicate logic between two networking layers.

Next, we needed to find a solution for reconciling common data models created by the GraphQL layer with the data received from our REST endpoints. The Apollo client automatically generates a model based on the GraphQL queries passed in.  The models created are namespaced to their corresponding queries. This creates the possibility for two identical queries requesting the same data, with the same raw response, but two completely different namespaced models. This scenario is unlikely but proved problematic with our hybrid approach.

In order to work around this issue, we decided to treat the models created by the Apollo SDK as raw data.  For a domain model that did not previously exist, we created a new model that mirrored the data from the GraphQL model.  For pre-existing domain models, we created a protocol implemented by the automatically generated GraphQL models to convert them to an existing domain model. This allowed us to use a single, consistent representation of the data throughout our codebase.

Moving Forward

While integrating GraphQL did not come without challenges, we are pleased with the results and our future outlook. We’ve drastically lessened the impact of upstream decisions on our mobile apps, improved performance, and created cross-platform parity. The consolidation of our API into a single GraphQL layer will decrease the time spent maintaining multiple APIs, allowing our team to focus on building products that help consumers find a place they love to live.

You can read about how our web applications are also benefiting from our decision to implement GraphQL, as well as other topics related to our transition to a microservices architecture at our Tech and Innovation blog.