Module 5 · Lesson 1 · ~20 min read
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.
.proto file, and a code generator emits language-specific stubs (Go, Java, Python, Scala, etc.) that marshal/unmarshal to a compact binary wire format.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.
| HTTP+JSON (REST) | gRPC + protobuf | |
|---|---|---|
| Schema | Optional (OpenAPI), drift-prone | Source of truth in .proto; generated stubs match exactly |
| Wire format | JSON: human-readable, ~5x larger than needed, slow to parse | Binary, compact, fast to parse |
| Transport | HTTP/1.1 typical (or HTTP/2 if both ends agree) | HTTP/2 mandatory — multiplexed streams over one TCP connection |
| Streaming | SSE / chunked transfer / WebSockets — bolted on | First-class: server, client, and bidirectional streams as method types |
| Method signatures | Verbs in URLs, conventions vary | Strongly-typed RPC methods in the schema |
| Cross-language clients | Hand-written or generated from OpenAPI | Generated from the same .proto for every supported language |
| Browser support | Native | Needs 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.
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.
A gRPC call is an HTTP/2 request with:
/<package>.<Service>/<Method> — e.g., /com.daml.ledger.api.v2.CommandService/SubmitAndWait.content-type: application/grpc+proto, plus authentication tokens, trace IDs, etc.OK, NOT_FOUND, UNAVAILABLE, etc.) sent in HTTP/2 trailers.You won't usually deal with this directly — generated stubs hide it. But when debugging with grpcurl or tcpdump, knowing the wire shape helps.
Three libraries you'll touch:
| Library | What it is |
|---|---|
google.golang.org/grpc | The Go gRPC runtime — clients, servers, interceptors, dial options. |
google.golang.org/protobuf | Protobuf runtime — message marshaling. Replaces the older github.com/golang/protobuf. |
google.golang.org/grpc/credentials | TLS, 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.
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.
Two non-obvious things to internalize:
optional keyword (proto3 since 3.15) or wrap in a message type.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.
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.)
Treat resp == nil as success. If err == nil, resp is meaningful. Always check err first.
google.golang.org/grpc + google.golang.org/protobuf + the two protoc plugins.ClientConn, deadlines on every call, error-first checking.