gRPC Calculator
Code Walkthrough — All 4 Cases
Unary · Server Streaming · Client Streaming · Bidirectional
Quick Map — Which stub? Which args? What returns?
Case Method Stub Args passed Returns
1 Unary getSquare() blockingStub request only NumberResponse (direct)
2 Server Streaming getMultiplicationTable() blockingStub request only Iterator<NumberResponse>
3 Client Streaming getSum() asyncStub responseObserver requestObserver
4 Bidi Streaming getRunningSum() asyncStub responseObserver requestObserver
Unary — getSquare()
One request → One response. Simplest gRPC.
1
What it does
Client sends the number 7. Server squares it and returns 49. The client blocks (waits) until the response arrives. No
streaming, no observers, no callbacks.
Proto definition
rpc getSquare(NumberRequest) returns (NumberResponse); // ONE in, ONE out — no stream keyword
Code — line by line
public void getSquare(int number) {
NumberRequest request = [Link]() // Build the protobuf request object
.setNumber(number) // e.g. 7 // Set number = 7
.build();
NumberResponse response = [Link](request); // blockingStub → thread BLOCKS here
// ↑ thread pauses here until server responds
[Link]([Link]()); // Prints: 'Square of 7 is 49'
Step-by-step flow
CLIENT Builds NumberRequest with number=7
NETWORK [Link](request) → sends request over gRPC
SERVER Receives request, computes 7×7=49, builds NumberResponse
NETWORK Response sent back across wire
CLIENT blockingStub returns the NumberResponse. Thread unblocks. Print result.
KEY blockingStub means the calling thread WAITS. No callback, no observer, no latch needed. The return value IS the
response.
Server Streaming — getMultiplicationTable()
One request → Many responses via Iterator
2
What it does
Client sends number 5. Server responds with 10 lines: 5×1=5, 5×2=10, ... 5×10=50. Each line arrives separately. Client
reads them one by one using an Iterator. ONLY the server streams — client sends exactly one request.
Proto definition
rpc getMultiplicationTable(NumberRequest) returns (stream // 'stream' keyword only on response side
NumberResponse);
Why blockingStub here, not asyncStub?
blockingStub hides the StreamObserver machinery for you. It converts the server's onNext() calls into Iterator entries
internally. You just call [Link]() — simple, synchronous, no callbacks.
Code — line by line
public void getMultiplicationTable(int number) {
NumberRequest request = [Link]() // Build request — same as unary
.setNumber(number)
.build();
Iterator it = // gRPC returns Iterator, not one object
[Link](request); // ONE arg: just request. No observer!
// ↑ client sends request, server starts streaming back
while ([Link]()) { // hasNext() = false when server calls onCompleted()
NumberResponse res = [Link](); // Each next() = one server onNext()
[Link]([Link]()); // Prints: 5x1=5, 5x2=10, ...
} // Loop ends automatically
The invisible mapping
Server does Client sees
[Link](5×1=5) ■■■■ [Link]() returns first response
[Link](5×2=10) ■■■■ [Link]() returns second response
... ■■■■ ...
[Link]() ■■■■ [Link]() returns false
KEY Client passes ONLY the request — no responseObserver! blockingStub creates the hidden observer and wraps it
as an Iterator for you. This is WHY you use blockingStub for server streaming, not asyncStub.
Client Streaming — getSum()
Many requests → One response. Mailbox exchange.
3
What it does
Client sends numbers 1, 2, 3, 4, 5 one at a time. Server accumulates: sum = 1+2+3+4+5 = 15. When client signals
done, server sends back ONE response: 15. ONLY the client streams — server replies exactly once.
Proto definition
rpc getSum(stream NumberRequest) returns (NumberResponse); // 'stream' keyword only on request side
Why asyncStub here, not blockingStub?
Because you need a requestObserver to push numbers into the server. [Link]() does the mailbox
exchange: you give your responseObserver, it gives you back the server's requestObserver. blockingStub cannot do
this — it has no concept of streaming requests.
What is CountDownLatch?
asyncStub works on a background thread. Main thread would exit before the server responds. CountDownLatch(1)
makes main thread wait. [Link]() is called inside onCompleted() — releasing the main thread only when
server is done.
Code — line by line
public void getSum(int[] numbers) throws
InterruptedException {
CountDownLatch latch = new CountDownLatch(1); // Gate: main thread waits here
// STEP 1: Client builds its mailbox (responseObserver)
// and passes it to the server via [Link]()
StreamObserver requestObserver = // We get server's mailbox back
[Link](
new StreamObserver() { // ← THIS is client's mailbox
@Override
public void onNext(NumberResponse response) { // Server wrote into our mailbox
[Link]([Link]()); // Prints: 'Sum is 15'
@Override
public void onError(Throwable t) {
[Link](); // Release latch on error too
@Override
public void onCompleted() { // Server finished writing
[Link]('Streaming completed');
[Link](); // release main thread // Unblocks [Link]()
}); // Mailbox exchange done here
// STEP 2: Client streams numbers into server's mailbox
for (int number : numbers) { // numbers = {1,2,3,4,5}
[Link]( // Write into SERVER's mailbox
[Link]()
.setNumber(number).build());
// STEP 3: Signal server we are done
[Link](); // Triggers server's onCompleted()
// ↑ server's sum is now final. Server calls
[Link](15)
[Link](); // wait for server's onCompleted callback // Main thread blocks here
The mailbox exchange — visualised
responseObserver requestObserver
Built by Client (anonymous class) Server (returned from getSum())
Held by Server as parameter Client as return value
Written by Server → onNext(sum=15) Client → onNext(each number)
Triggers Client's onNext() callback Server's onNext() — sum+=number
Done signal [Link]() [Link]()
KEY sum=0 is born the moment [Link]() is called — inside the anonymous class constructor. It lives until
the anonymous class is garbage collected after onCompleted(). A second call to getSum() creates a brand new
sum=0 with no memory of the first.
Bidirectional Streaming — getRunningSum()
Many requests → Many responses. Both sides stream simultaneously.
4
What it does
Client sends numbers 10, 20, 30, 40. After EACH number, the server immediately replies with the running sum: 10, 30,
60, 100. Both sides stream at the same time. Both sides have a mailbox. This is the full symmetric model your notes
describe perfectly.
Proto definition
rpc getRunningSum(stream NumberRequest) returns (stream // 'stream' on BOTH sides
NumberResponse);
How is this different from Client Streaming (Case 3)?
Case 3 — Client Streaming Case 4 — Bidi Streaming
Server replies ONCE — only on client's onCompleted() After EACH number received
Server's onNext() Never called mid-stream Called after every client onNext()
Interleaving No — sequential Yes — simultaneous both directions
[Link](200) Not needed Added to SEE interleaving in output
Code — line by line
public void getRunningSum(int[] numbers) throws
InterruptedException {
CountDownLatch latch = new CountDownLatch(1); // Same gate as Case 3
// STEP 1: Client builds responseObserver, gets
requestObserver back
StreamObserver requestObserver =
[Link](
new StreamObserver() { // Client's mailbox
@Override
public void onNext(NumberResponse response) { // Server wrote here after EACH number
// called once per number, not once total
[Link]('Running sum: '
+ [Link]()); // Prints: 10, 30, 60, 100
@Override
public void onCompleted() {
[Link]('Bidi Streaming completed');
[Link](); // Release main thread
}); // Mailbox exchange done
// STEP 2: Stream numbers, server replies after each one
for (int number : numbers) { // numbers = {10,20,30,40}
[Link]( // Write into server's mailbox
[Link]()
.setNumber(number).build());
[Link](200); // see interleaving in console // Not required — just demo
[Link](); // Signal we're done sending
[Link](); // Wait for server's onCompleted
Bidi interleave — what the console shows
Client sends Server receives Server replies Client onNext() prints
10 sum=10 onNext(10) Running sum: 10
20 sum=30 onNext(30) Running sum: 30
30 sum=60 onNext(60) Running sum: 60
40 sum=100 onNext(100) Running sum: 100
onCompleted() — onCompleted() Bidi Streaming completed
KEY Bidi is identical to Client Streaming in CODE STRUCTURE — same asyncStub, same latch, same
requestObserver pattern. The ONLY difference is the server calls [Link]() inside its own
onNext(), not inside onCompleted(). That one change makes it bidirectional.
MASTER CHEATSHEET — All 4 Cases Side by Side
Case 1 Case 2 Case 3 Case 4
Unary Server Stream Client Stream Bidi Stream
Stub blockingStub blockingStub asyncStub asyncStub
Args to method request request responseObserver responseObserver
Returns NumberResponse Iterator<> requestObserver requestObserver
CountDownLatch? No No Yes Yes
responseObserver? No Hidden (Iterator) Yes — anonymous classYes — anonymous class
requestObserver? No No Yes — loop + onCompleted
Yes — loop + onCompleted
Server replies Once immediately Stream of N responses
Once on onCompleted()After every onNext()
Client reads via Return value [Link]() onNext() callback onNext() callback
Thread behaviour Main blocks Main blocks per next()
Main blocks on latch Main blocks on latch
Golden Rule: You never write into your own mailbox. You always write into the one you received from the other side. In Cases 3
& 4, client writes into requestObserver (server's mailbox). Server writes into responseObserver (client's mailbox).