Module 5 · Lesson 1 · ~20 min read

Why gRPC? Why Protobuf?

Canton's Ledger API is gRPC. Every Go-side integration with Canton speaks gRPC. This module is your bridge from "I read the lessons" to "I can build a client." Lesson 1: why this technology stack, and what to expect when you sit down to write the code.

Two technologies, jointly

You can use protobuf without gRPC (just for messages on Kafka, in files, etc.). You shouldn't use gRPC without protobuf — they're designed together.

What gRPC gives you over plain HTTP+JSON

HTTP+JSON (REST)gRPC + protobuf
SchemaOptional (OpenAPI), drift-proneSource of truth in .proto; generated stubs match exactly
Wire formatJSON: human-readable, ~5x larger than needed, slow to parseBinary, compact, fast to parse
TransportHTTP/1.1 typical (or HTTP/2 if both ends agree)HTTP/2 mandatory — multiplexed streams over one TCP connection
StreamingSSE / chunked transfer / WebSockets — bolted onFirst-class: server, client, and bidirectional streams as method types
Method signaturesVerbs in URLs, conventions varyStrongly-typed RPC methods in the schema
Cross-language clientsHand-written or generated from OpenAPIGenerated from the same .proto for every supported language
Browser supportNativeNeeds gRPC-Web or a JSON gateway

gRPC pays a learning curve up front; you get type safety, performance, and streaming in return. For service-to-service communication in a controlled environment (like Canton's participant ↔ external integration boundary), it's the right tool.

The four method types

gRPC supports four RPC patterns. Each shapes how your code looks.

service LedgerAPI {
    // 1. Unary: one request, one response
    rpc SubmitAndWait(SubmitRequest) returns (CompletionResponse);

    // 2. Server streaming: one request, stream of responses
    rpc GetTransactionStream(GetTransactionsRequest) returns (stream Transaction);

    // 3. Client streaming: stream of requests, one response
    rpc UploadDar(stream DarChunk) returns (UploadResponse);

    // 4. Bidirectional streaming: both sides stream
    rpc CompletionStream(stream CompletionRequest) returns (stream Completion);
}

Canton's Ledger API uses all four. The transaction stream is server streaming — you make one call to start, the server sends transactions forever (or until you cancel). Submission is unary or client-streaming depending on the variant.

Wire-level: a quick anatomy

A gRPC call is an HTTP/2 request with:

You won't usually deal with this directly — generated stubs hide it. But when debugging with grpcurl or tcpdump, knowing the wire shape helps.

The Go gRPC ecosystem

Three libraries you'll touch:

LibraryWhat it is
google.golang.org/grpcThe Go gRPC runtime — clients, servers, interceptors, dial options.
google.golang.org/protobufProtobuf runtime — message marshaling. Replaces the older github.com/golang/protobuf.
google.golang.org/grpc/credentialsTLS, OAuth, custom auth credential providers.

You'll also need the code generators: protoc-gen-go (generates message types) and protoc-gen-go-grpc (generates service stubs). Lesson 2 walks through the toolchain.

What "calling a Canton method in Go" actually looks like

Skipping ahead — this is what you'll write at the end of Module 5:

conn, err := grpc.NewClient("localhost:5011",
    grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { return err }
defer conn.Close()

client := ledger.NewCommandServiceClient(conn)

resp, err := client.SubmitAndWait(ctx, &ledger.SubmitRequest{
    Commands: cmds,
})

Once stubs are generated, the call site is plain Go. The hard parts are: setting up the connection (TLS, credentials, retries, keepalives), generating the stubs, and handling streaming methods correctly.

Why protobuf trips up newcomers

Two non-obvious things to internalize:

  1. Field numbers are the wire identity, not field names. Renaming a field is a free, backwards-compatible change. Renumbering a field is a wire-incompatible change. The generator preserves field numbers across edits to the schema; this is why protobuf is so stable across versions.
  2. Optional and presence are subtle. In proto3 (the version Canton uses), scalar fields default to their type's zero value. There's no built-in "is this field set?" — the value is the wire absence. For genuinely optional fields, use optional keyword (proto3 since 3.15) or wrap in a message type.

Common pitfalls — preview

Don't

Make a new gRPC connection per call. A grpc.ClientConn is meant to be long-lived; it manages its own connection pool. Re-creating it per request burns your connection budget and breaks keepalives.

Don't

Forget the deadline. Pass ctx with a timeout to every call. Without it, a hung server holds your goroutine forever. (Real Canton clients almost always set a deadline at the call site.)

Don't

Treat resp == nil as success. If err == nil, resp is meaningful. Always check err first.

Takeaways