Definition
- Unary – a single request with a single response, like a normal function call; declared without the
streamkeyword. - Server streaming – a single request with a stream of responses; declared with
streamon the return type. - Client streaming – a stream of requests with a single response; declared with
streamon the parameter. - Bidirectional streaming – both sides stream independently over one long-lived call; declared with
streamon both. - Flow control – HTTP/2 windowing prevents a fast sender from overwhelming a slow receiver across all streaming patterns.
At National Bank of India: Unary is Rajesh asking "what is my balance?" and getting one answer. Server streaming is Rajesh asking "show me my statement" and the teller reading out transactions one by one. Client streaming is Rajesh handing over a stack of cheques one at a time and the teller giving one receipt at the end. Bidirectional streaming is a live chat where Rajesh and the teller both speak freely on the same open line.
Understanding the Concepts
What are the four patterns in the .proto file?
All four patterns are declared using the stream keyword on the request side, the response side, both, or neither.
syntax = "proto3";
package com.bank;
service AccountService {
rpc GetBalance (AccountRef) returns (Balance); // unary
rpc StreamStatement (AccountRef) returns (stream Transaction); // server streaming
rpc DepositCheques (stream Cheque) returns (DepositSummary); // client streaming
rpc LiveTrading (stream Order) returns (stream Fill); // bidirectional
}
message AccountRef { string account_number = 1; }
message Balance { int64 amount_paise = 1; }
message Transaction { string txn_id = 1; int64 amount_paise = 2; string type = 3; }
message Cheque { string cheque_number = 1; int64 amount_paise = 2; }
message DepositSummary { int32 count = 1; int64 total_paise = 2; }
message Order { string symbol = 1; int64 quantity = 2; string side = 3; }
message Fill { string order_id = 1; int64 filled_quantity = 2; int64 price_paise = 3; }
What is unary (one request, one response)?
Unary is the simplest and most common pattern. The client sends one message and waits for one response, exactly like a local method call. It suits ordinary request-response operations like fetching a balance or placing a single transfer.
package com.bank.grpc;
import com.bank.AccountRef;
import com.bank.Balance;
import com.bank.AccountServiceGrpc;
import io.grpc.stub.StreamObserver;
public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBase {
@Override
public void getBalance(AccountRef request, StreamObserver<Balance> responseObserver) {
long balancePaise = accountRepo.getBalancePaise(request.getAccountNumber());
responseObserver.onNext(Balance.newBuilder().setAmountPaise(balancePaise).build());
responseObserver.onCompleted();
}
}
What is server streaming (one request, stream of responses)?
The client makes one call and the server pushes many messages over time, then completes. It suits large result sets, live price feeds, or progress updates. The client processes messages as they arrive rather than waiting for one huge payload.
@Override
public void streamStatement(AccountRef request, StreamObserver<Transaction> responseObserver) {
List<Transaction> txns = transactionRepo.findByAccount(request.getAccountNumber());
for (Transaction txn : txns) {
responseObserver.onNext(txn); // push each transaction as it is ready
}
responseObserver.onCompleted(); // signal end of stream
}
What is client streaming (stream of requests, one response)?
The client sends a stream of requests and the server returns a single response at the end. It suits batch uploads, aggregation, or feeding many data points to compute one result.
@Override
public StreamObserver<Cheque> depositCheques(StreamObserver<DepositSummary> responseObserver) {
return new StreamObserver<>() {
long totalPaise = 0;
int count = 0;
@Override
public void onNext(Cheque cheque) {
totalPaise += cheque.getAmountPaise();
count++;
}
@Override
public void onError(Throwable t) {
// handle error – partial deposit may need rollback
}
@Override
public void onCompleted() {
responseObserver.onNext(DepositSummary.newBuilder()
.setCount(count)
.setTotalPaise(totalPaise)
.build());
responseObserver.onCompleted();
}
};
}
What is bidirectional streaming (both sides stream)?
Both sides send messages independently over one long-lived call. The order is not lock-stepped; each side sends at its own pace. It suits real-time interactive scenarios like live trading or chat.
@Override
public StreamObserver<Order> liveTrading(StreamObserver<Fill> responseObserver) {
return new StreamObserver<>() {
@Override
public void onNext(Order order) {
// Process order and send fill back immediately
Fill fill = matchEngine.execute(order);
responseObserver.onNext(fill); // server streams fills as orders arrive
}
@Override
public void onError(Throwable t) {
// Connection dropped – reconcile open orders
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
How do you choose the right pattern? (comparison table)
| Pattern | Request | Response | Use case |
|---|---|---|---|
| Unary | 1 | 1 | Balance query, single transfer |
| Server streaming | 1 | Many | Long statement, price feed |
| Client streaming | Many | 1 | Batch cheque deposit, file upload |
| Bidirectional | Many | Many | Live trading, chat |
What about flow control and backpressure?
HTTP/2 provides windowing so a fast sender does not overwhelm a slow receiver. gRPC surfaces this so you can pause producing when the peer is not ready. This is critical in server streaming when pushing thousands of transactions: the server must respect the client's receive window.
// Server-side flow control check before sending:
if (responseObserver instanceof ServerCallStreamObserver) {
ServerCallStreamObserver<Transaction> obs =
(ServerCallStreamObserver<Transaction>) responseObserver;
obs.setOnReadyHandler(() -> {
while (obs.isReady() && hasMore()) {
obs.onNext(nextTransaction());
}
});
}
What about error handling in streams?
A stream can fail partway, so both sides implement onError. In banking, a client-streaming cheque deposit must decide whether partial work is committed or rolled back. A bidirectional trading stream must reconcile exactly which orders were filled if the connection drops.
@Override
public void onError(Throwable t) {
if (t instanceof StatusRuntimeException sre) {
if (sre.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
// Client timed out – rollback partial deposit
rollbackPartialDeposit();
}
}
}
What should a senior engineer know?
Choose the pattern by data shape, not habit. If the result is one value, use unary. If one input yields many outputs, use server streaming. If many inputs yield one aggregate, use client streaming. If the interaction is genuinely conversational, use bidirectional. Overusing bidirectional where unary would do adds complexity for no benefit. Forcing a streaming workload into repeated unary calls wastes the efficiency HTTP/2 streams provide.
Visual Overview
Four gRPC Patterns:
Unary: Client ──req──→ Server ──resp──→ Client
Server Streaming: Client ──req──→ Server ──resp1──→
──resp2──→
──resp3──→ Client
Client Streaming: Client ──req1──→
──req2──→
──req3──→ Server ──resp──→ Client
Bidirectional: Client ──req1──→ ←──resp1── Server
──req2──→ ←──resp2──
──req3──→ ←──resp3──
Key Takeaway
gRPC has four communication patterns, all built on HTTP/2 streams and declared in the proto file with the stream keyword. Unary is one request and one response, fitting ordinary request-response work like fetching a balance. Server streaming is one request and a stream of responses, fitting large result sets like a long account statement where the client processes messages as they arrive. Client streaming reverses that: many requests and one response at the end, fitting batch operations like a corporate cheque deposit. Bidirectional streaming lets both sides send independently, fitting genuinely conversational real-time scenarios like a live trading session.
All streaming patterns share two concerns: flow control from HTTP/2 windowing so a fast sender does not overwhelm a slow receiver, and error handling through onError so both sides decide whether partial work is committed or rolled back. At National Bank of India, each feature picked the matching pattern: a balance query is unary, a long statement is server streaming, a corporate batch deposit is client streaming, and a trader's live session is bidirectional.
The judgement to bring is choosing by the shape of the data, not by habit. If the result is one value, use unary. If one input yields many outputs, use server streaming. If many inputs yield one aggregate, use client streaming. Only when the interaction is truly concurrent and conversational use bidirectional, because it is the most powerful but also the most complex to get right.
Remember one sentence: match the gRPC pattern to the shape of the data – one-to-one, one-to-many, many-to-one, or many-to-many – and the code stays simple and efficient.
---