Lesson 74 of 77 advanced

gRPC, Protobuf & Advanced API Patterns

High-Performance Communication Beyond REST

Open interactive version (quiz + challenge)

Real-world analogy

Imagine REST is like sending letters through the post office — you write everything in plain English (JSON), stuff it in an envelope, and hope the recipient understands the format. gRPC is like a dedicated phone line with a shared codebook — both sides agree on a strict binary protocol (Protobuf) beforehand, the messages are compressed and tiny, and you can even keep the line open for continuous two-way conversation (bidirectional streaming). The phone line is faster, but you need both parties to have the same codebook.

What is it?

gRPC (Google Remote Procedure Call) is a high-performance, open-source RPC framework that uses Protocol Buffers for serialization and HTTP/2 for transport. It enables type-safe, binary communication between services with support for four communication patterns: unary (request-response), server streaming, client streaming, and bidirectional streaming. In Flutter, the grpc-dart package provides generated client stubs that make calling remote services as simple as calling local methods.

Real-world relevance

gRPC is used in production at Google (internally for almost all services), Netflix (inter-microservice communication), Square (mobile to backend), and Alibaba (high-throughput trading systems). In Flutter apps, gRPC excels for: real-time features (chat, live updates via bidirectional streaming), IoT device communication, internal microservice calls, and any scenario where performance matters more than human-readability. Many apps use gRPC for critical paths and REST for everything else.

Key points

Code example

// 1. Proto file: user_service.proto
// syntax = "proto3";
// package users;
//
// service UserService {
//   rpc GetUser (GetUserRequest) returns (User);
//   rpc ListUsers (ListUsersRequest) returns (stream User);
//   rpc CreateUsers (stream CreateUserRequest) returns (BatchResult);
//   rpc Chat (stream ChatMessage) returns (stream ChatMessage);
// }
//
// message User {
//   int32 id = 1;
//   string name = 2;
//   string email = 3;
// }

// 2. Generated code usage in Flutter
import 'package:grpc/grpc.dart';
import 'generated/user_service.pbgrpc.dart';
import 'generated/user_service.pb.dart';

// --- Auth Interceptor ---
class AuthInterceptor implements ClientInterceptor {
  final String Function() tokenProvider;

  AuthInterceptor(this.tokenProvider);

  @override
  ResponseFuture<R> interceptUnary<Q, R>(
    ClientMethod<Q, R> method,
    Q request,
    CallOptions options,
    ClientUnaryInvoker<Q, R> invoker,
  ) {
    final newOptions = options.mergedWith(
      CallOptions(metadata: {'authorization': 'Bearer ${tokenProvider()}'}),
    );
    return invoker(method, request, newOptions);
  }

  @override
  ResponseStream<R> interceptStreaming<Q, R>(
    ClientMethod<Q, R> method,
    Stream<Q> requests,
    CallOptions options,
    ClientStreamingInvoker<Q, R> invoker,
  ) {
    final newOptions = options.mergedWith(
      CallOptions(metadata: {'authorization': 'Bearer ${tokenProvider()}'}),
    );
    return invoker(method, requests, newOptions);
  }
}

// --- gRPC Client Manager ---
class GrpcClientManager {
  late final ClientChannel _channel;
  late final UserServiceClient _userService;

  GrpcClientManager({
    required String host,
    required int port,
    required String Function() tokenProvider,
  }) {
    _channel = ClientChannel(
      host,
      port: port,
      options: const ChannelOptions(
        credentials: ChannelCredentials.secure(), // TLS
        idleTimeout: Duration(minutes: 5),
      ),
    );

    _userService = UserServiceClient(
      _channel,
      options: CallOptions(timeout: Duration(seconds: 30)),
      interceptors: [AuthInterceptor(tokenProvider)],
    );
  }

  // Unary call
  Future<User> getUser(int id) async {
    try {
      return await _userService.getUser(
        GetUserRequest()..id = id,
      );
    } on GrpcError catch (e) {
      switch (e.code) {
        case StatusCode.notFound:
          throw UserNotFoundException('User $id not found');
        case StatusCode.permissionDenied:
          throw UnauthorizedException(e.message ?? 'Access denied');
        case StatusCode.deadlineExceeded:
          throw TimeoutException('Request timed out');
        default:
          throw ApiException('gRPC error: ${e.code} - ${e.message}');
      }
    }
  }

  // Server streaming
  Stream<User> listUsers({int pageSize = 20}) {
    return _userService.listUsers(
      ListUsersRequest()..pageSize = pageSize,
    );
  }

  // Bidirectional streaming
  Stream<ChatMessage> chat(Stream<ChatMessage> outgoing) {
    return _userService.chat(outgoing);
  }

  Future<void> shutdown() => _channel.shutdown();
}

// --- Usage ---
void main() async {
  final client = GrpcClientManager(
    host: 'api.example.com',
    port: 443,
    tokenProvider: () => SecureStorage.getToken(),
  );

  // Unary
  final user = await client.getUser(42);
  print('Got: ${user.name}');

  // Server streaming
  await for (final user in client.listUsers()) {
    print('User: ${user.name}');
  }

  await client.shutdown();
}

Line-by-line walkthrough

  1. 1. The .proto file defines the contract — UserService with 4 RPC methods showing all gRPC patterns: unary, server streaming, client streaming, and bidirectional.
  2. 2. AuthInterceptor implements ClientInterceptor to inject JWT tokens into every call automatically — both unary and streaming calls get the authorization metadata.
  3. 3. interceptUnary wraps single request/response calls. We merge new metadata (the auth token) into existing CallOptions, preserving any options already set.
  4. 4. interceptStreaming handles all streaming patterns (server, client, bidirectional) — the same token injection applies to streaming calls.
  5. 5. GrpcClientManager creates a ClientChannel with TLS credentials and idle timeout — the channel manages the underlying HTTP/2 connection.
  6. 6. UserServiceClient is the generated stub — we pass it the channel, default timeout (30 seconds), and our auth interceptor.
  7. 7. getUser() demonstrates proper gRPC error handling — catching GrpcError and mapping status codes to domain-specific exceptions.
  8. 8. listUsers() returns the server stream directly — the caller can use 'await for' to process users as they arrive from the server.
  9. 9. The chat() method passes an outgoing stream and returns an incoming stream — true bidirectional communication.
  10. 10. shutdown() cleanly closes the channel — important for mobile apps to release resources when the gRPC client is no longer needed.

Spot the bug

class GrpcService {
  UserServiceClient? _client;

  Future<User> getUser(int id) async {
    // Bug 1: Creating channel on every call
    final channel = ClientChannel(
      'api.example.com',
      port: 443,
      options: ChannelOptions(
        credentials: ChannelCredentials.insecure(), // Bug 2
      ),
    );

    _client = UserServiceClient(channel);

    // Bug 3: No error handling, no timeout
    final user = await _client!.getUser(
      GetUserRequest()..id = id,
    );

    return user;
    // Bug 4: Channel never shut down
  }
}
Need a hint?
There are four issues: connection management, security, error handling, and resource cleanup.
Show answer
1) Creating a new ClientChannel on every call wastes resources and loses HTTP/2 multiplexing benefits — create the channel once and reuse it. 2) ChannelCredentials.insecure() sends data unencrypted — use ChannelCredentials.secure() for production (TLS). 3) No try/catch for GrpcError and no timeout — add CallOptions(timeout: Duration(seconds: 30)) and handle status codes. 4) The channel is never shut down — connections leak. Store the channel as a field and provide a shutdown() method.

Explain like I'm 5

Imagine you and your friend invented a secret code where 'A' means apple, 'B' means banana, and so on. Instead of writing long letters saying 'Please send me three apples and two bananas,' you just write 'A3B2.' It's way shorter and faster! That's Protobuf — a compact code both sides understand. Now imagine you also have walkie-talkies where you can both talk at the same time — that's gRPC streaming. Regular REST is like passing notes in class — one at a time, and you have to write everything out in full English.

Fun fact

Google handles over 10 billion gRPC calls per second internally — more than any other RPC framework in existence. Protocol Buffers were originally developed at Google in 2001 (before gRPC existed in 2015), and the 'g' in gRPC doesn't officially stand for 'Google' — the team changes what it stands for with each release (gRPC 1.1 = 'good', 1.2 = 'green', 1.30 = 'groovy').

Hands-on challenge

Build a complete gRPC chat feature in Flutter. Define a chat.proto with ChatMessage (sender, text, timestamp) and a ChatService with a bidirectional Chat rpc. Generate the Dart code. Create a ChatRepository that manages the ClientChannel, handles reconnection on network changes, and exposes a Stream for incoming messages and a sendMessage() method. Add an interceptor that attaches a user-id metadata header. Handle UNAVAILABLE errors with exponential backoff retry. Write unit tests mocking the generated client stub.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery