Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions _docs/02-communications.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,10 @@ produces and fill in the one method, never touching the wire format itself.
{% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %}

```java
// inventory-service (excerpt): grpc-java server; protobuf-maven-plugin generates the stubs
@SpringBootApplication
public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase
implements CommandLineRunner {
@Override public void run(String... args) throws Exception {
server = ServerBuilder.forPort(grpcPort).addService(this).build().start();
server.awaitTermination();
}
// inventory-service (excerpt): a @GrpcService bean; spring-boot-starter-grpc-server hosts it on
// spring.grpc.server.port (graceful shutdown, standard health service); protoc generates the stubs
@GrpcService
public class InventoryService extends InventoryGrpc.InventoryImplBase {
@Override public void reserveStock(ReserveRequest req, StreamObserver<ReserveReply> obs) {
stock.putIfAbsent(req.getSku(), initialStock);
int remaining = stock.get(req.getSku()) - req.getQuantity();
Expand All @@ -264,6 +260,10 @@ public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase
obs.onCompleted();
}
}
// order-service: the channel comes from spring.grpc.client.channel.inventory.target
@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) {
return InventoryGrpc.newBlockingStub(channels.createChannel("inventory"));
}
```

```java
Expand Down
7 changes: 4 additions & 3 deletions _docs/03-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ the gateway is only the composition layer.
@Controller // spring-boot-starter-graphql
public class OrderGraphQLController {
private final OrderClient orderClient;
private final InventoryGrpcClient inventory;
public OrderGraphQLController(OrderClient o, InventoryGrpcClient i) {
private final InventoryGrpc.InventoryBlockingStub inventory; // Spring gRPC channel bean
public OrderGraphQLController(OrderClient o, InventoryGrpc.InventoryBlockingStub i) {
this.orderClient = o; this.inventory = i;
}

Expand All @@ -76,7 +76,8 @@ public class OrderGraphQLController {

@SchemaMapping(typeName = "Order", field = "stock") // resolved on demand
public Integer stock(Order order) { // via gRPC
return inventory.remaining(order.sku());
return inventory.getStock(GetStockRequest.newBuilder().setSku(order.sku()).build())
.getAvailable();
}
}
```
Expand Down
6 changes: 4 additions & 2 deletions _docs/05-event-driven.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,10 @@ the service:

```java
// the sidecar (localhost, same pod) owns Kafka — no broker client here
@GrpcClient("sidecar")
private EventBusGrpc.EventBusBlockingStub sidecar;
// Spring gRPC channel: spring.grpc.client.channel.sidecar.target=localhost:50051
@Bean EventBusGrpc.EventBusBlockingStub sidecar(GrpcChannelFactory channels) {
return EventBusGrpc.newBlockingStub(channels.createChannel("sidecar"));
}

void emit(Order o) {
sidecar.publish(PublishRequest.newBuilder()
Expand Down
7 changes: 4 additions & 3 deletions _docs/11-observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ ENV JAVA_TOOL_OPTIONS="-javaagent:/deployments/opentelemetry-javaagent.jar"
// OpenTelemetry API annotation (io.opentelemetry.instrumentation.annotations):
@Service
public class OrderService {
private final InventoryGrpcClient inventory;
public OrderService(InventoryGrpcClient i) { this.inventory = i; }
private final InventoryServiceGrpc.InventoryServiceBlockingStub inventory; // Spring gRPC
public OrderService(InventoryServiceGrpc.InventoryServiceBlockingStub i) { this.inventory = i; }

@WithSpan("reserve_stock") // custom span
public ReserveReply reserve(String sku, int qty) {
return inventory.reserveStock(sku, qty);
return inventory.reserveStock(
ReserveRequest.newBuilder().setSku(sku).setQuantity(qty).build());
}
// trace_id flows: REST -> gRPC -> Kafka -> consumer
}
Expand Down
11 changes: 6 additions & 5 deletions _docs/14-appendix-a-protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ each language gets a generated client from the one `.proto`.
{% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %}

```java
// a Spring bean wraps the generated blocking stub (grpc-java, plaintext channel)
@Bean InventoryGrpc.InventoryBlockingStub inventoryStub() {
return InventoryGrpc.newBlockingStub(
ManagedChannelBuilder.forAddress(host, port).usePlaintext().build());
// a Spring bean wraps the generated blocking stub; Spring gRPC builds and owns the channel
// (spring.grpc.client.channel.inventory.target, plaintext inside the cluster)
@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) {
return InventoryGrpc.newBlockingStub(channels.createChannel("inventory"));
}
// constructor-injected: OrderService(InventoryGrpc.InventoryBlockingStub inventory)

Expand Down Expand Up @@ -180,7 +180,8 @@ note how every one expresses "push many, then finish."
{% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %}

```java
// server-streaming RPC; registered with ServerBuilder.forPort(port).addService(...)
// server-streaming RPC; Spring gRPC registers the @GrpcService bean on its server
@GrpcService
public class OrderEvents extends OrderEventsGrpc.OrderEventsImplBase {
@Override
public void streamOrderEvents(StreamRequest req,
Expand Down
9 changes: 5 additions & 4 deletions _plans/s14-resilience-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ language; do not hand-roll what a maintained library provides.
| Spring Boot 4.1.1 | `io.github.resilience4j:resilience4j-spring-boot4` | 2.4.0 (built against Boot 4.0.0) | Maven Central |
| Spring Boot 4.1.1 | `spring-boot-starter-aspectj` (the Boot 4 name of the AOP starter) | BOM 4.1.1 | Boot BOM |
| Spring Boot 4.1.1 | `spring-boot-starter-grpc-server` / `-client` (Spring gRPC) | 1.1.1 via BOM 4.1.1 | Boot BOM |
| Spring Boot 4.1.1 | `io.github.ascopes:protobuf-maven-plugin` | 5.1.8 via BOM 4.1.1 | Boot BOM |
| Spring Boot 4.1.1 | `io.github.ascopes:protobuf-maven-plugin` | not adopted: fails under the UBI packaged Maven; xolstice 0.6.1 stays | Boot BOM |
| Quarkus 3.40.1 | `quarkus-smallrye-fault-tolerance` | BOM 3.40.1 | Quarkus BOM |
| .NET 10 | `Microsoft.Extensions.Http.Resilience` | 10.10.0 (Polly.Core 8.8.0) | NuGet |
| .NET 10 | `Grpc.Net.ClientFactory` | 2.84.0 (matches Grpc.Net.Client) | NuGet |
Expand Down Expand Up @@ -120,8 +120,9 @@ example has. Deck slides per language: bulkhead [085] (.NET [087]); Appendix M t
- **Spring gRPC:** `spring-boot-starter-grpc-server` in the four inventory services (`@Service` beans extending the
generated `ImplBase`, `spring.grpc.server.port=50051`, graceful shutdown by the starter, standard health service),
`spring-boot-starter-grpc-client` in the callers (`GrpcChannelFactory` / `@ImportGrpcClients`,
`spring.grpc.client.channels.inventory.address`), the protobuf plugin switched to the Boot-managed
`io.github.ascopes` plugin. 18 keeps its deadline (`withDeadlineAfter`) and its `StatusRuntimeException` mapping.
`spring.grpc.client.channels.inventory.address`), the protobuf plugin stays xolstice 0.6.1:
the Boot-managed `io.github.ascopes` plugin fails to load under the UBI image's packaged Maven (Guice cannot
find ASM), and replacing Maven in every Java image is out of scope. 18 keeps its deadline (`withDeadlineAfter`) and its `StatusRuntimeException` mapping.
The OTel agent keeps instrumenting grpc-java; 11's cross-service trace check is the acceptance gate.

## PRs (sequential, each merged green before the next)
Expand All @@ -144,7 +145,7 @@ Order inside a PR: Spring, Quarkus, .NET, Python, Go, C++, one example at a time
| 0 | Preflight: user decisions D1–D4 (done 2026-10-13); pins (done, see Pins); baseline matrix of 12, 26, 02, 03, 11, 18 on main | – | done: baseline 36/36 PASS on main 6656c0a code (2026-10-13) |
| 1 | Example 26 + verify.sh + Appendix M prose + decks | 1 | done: 26 × 6 PASS (22/22 each); Appendix M tabs, READMEs, 18 deck slides |
| 2 | Example 12 bulkhead + verify.sh + chapter 12 prose + decks | 2 | done: 12 × 6 PASS (18/18 each); chapter 12 tab, READMEs, 6 deck slides |
| 3 | Spring gRPC (+ .NET client factory) + gRPC prose and slide fixes | 3 | pending |
| 3 | Spring gRPC (+ .NET client factory) + gRPC prose and slide fixes | 3 | done: 02, 03, 11, 18 Spring and .NET PASS; gRPC health SERVING; protobuf plugin stays xolstice (see Approach) |
| 4 | CLAUDE.md, modernization plan, final 111 matrix, footers | 4 | pending |

## Acceptance criteria
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.12" />
<PackageReference Include="Confluent.Kafka" Version="2.16.0" />
<PackageReference Include="HotChocolate.AspNetCore" Version="16.6.8" />
<PackageReference Include="Grpc.Net.Client" Version="2.84.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.84.0" />
<PackageReference Include="Google.Protobuf" Version="3.36.2" />
<PackageReference Include="Grpc.Tools" Version="2.84.0" PrivateAssets="All" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.19.1" />
Expand Down
10 changes: 4 additions & 6 deletions examples/02-communications/dotnet/OrderService/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json;
using Confluent.Kafka;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Npgsql;
Expand Down Expand Up @@ -30,13 +29,12 @@
.AddQueryType<Query>();

builder.Services.AddHealthChecks();
// gRPC client factory: the channel and its HTTP/2 handler are managed like any typed HttpClient.
builder.Services.AddGrpcClient<Inventory.InventoryClient>(o => o.Address = new Uri($"http://{inventoryAddr}"))
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { EnableMultipleHttp2Connections = true });

var app = builder.Build();

var channel = GrpcChannel.ForAddress($"http://{inventoryAddr}",
new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler { EnableMultipleHttp2Connections = true } });
var inventoryClient = new Inventory.InventoryClient(channel);

IProducer<Null, string>? producer = null;
for (var i = 0; i < 30; i++)
{
Expand All @@ -54,7 +52,7 @@
ResponseWriter = (ctx, _) => ctx.Response.WriteAsJsonAsync(new { status = "ok" }),
});

app.MapPost("/orders", async (HttpContext ctx, OrdersContext db) =>
app.MapPost("/orders", async (HttpContext ctx, OrdersContext db, Inventory.InventoryClient inventoryClient) =>
{
using var doc = await JsonDocument.ParseAsync(ctx.Request.Body);
var root = doc.RootElement;
Expand Down
4 changes: 2 additions & 2 deletions examples/02-communications/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Four communication styles — REST, gRPC, async events (Kafka), and GraphQL —
## Framework & libraries

- **Framework**: ASP.NET Core (.NET 10.0.12) minimal API
- **Libraries**: HotChocolate.AspNetCore (GraphQL), Confluent.Kafka 2.16.0, Grpc.Net.Client, Google.Protobuf
- **Libraries**: HotChocolate.AspNetCore (GraphQL), Confluent.Kafka 2.16.0, Grpc.Net.ClientFactory (`AddGrpcClient`), Google.Protobuf
- **Container base**: registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 (runtime), built with ubi9/dotnet-100:9.8-1791421718; runs as UID 1001

## Run
Expand Down Expand Up @@ -35,7 +35,7 @@ GraphQL via HotChocolate (code-first). gRPC stubs generated via Grpc.Tools at bu

Key variables (set by compose.yaml, override for local dev):

`DATABASE_URL, KAFKA_BOOTSTRAP, INVENTORY_GRPC_HOST`
`DATABASE_URL, KAFKA_BOOTSTRAP, INVENTORY_ADDR`

## Local development (without containers)

Expand Down
8 changes: 4 additions & 4 deletions examples/02-communications/spring-boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Four communication styles — REST, gRPC, async events (Kafka), and GraphQL —
## Framework & libraries

- **Framework**: Spring Boot 4.1.1, Java 25 (Web, GraphQL, gRPC)
- **Libraries**: spring-boot-starter-graphql, spring-boot-starter-kafka (Spring Kafka 4), grpc-netty-shaded, grpc-protobuf, OpenTelemetry Java agent 2.32.0
- **Libraries**: spring-boot-starter-graphql, spring-boot-starter-kafka (Spring Kafka 4), spring-boot-starter-grpc-server (inventory) and spring-boot-starter-grpc-client (callers), Spring gRPC 1.1.1, OpenTelemetry Java agent 2.32.0
- **Container base**: `ubi10/openjdk-25-runtime:1.24-15` (UID 185)

## Run
Expand All @@ -24,8 +24,8 @@ cd .. && bash verify.sh

| Path | Description |
|------|-------------|
| `order-service/` | OrderController.java — REST + GraphQL; KafkaProducerConfig — async events; InventoryGrpcClient — gRPC calls |
| `inventory-service/` | InventoryGrpcService.java — gRPC server |
| `order-service/` | OrderController.java — REST + GraphQL; KafkaProducerConfig — async events; inventory stub from Spring gRPC's GrpcChannelFactory — gRPC calls |
| `inventory-service/` | InventoryService.java — `@GrpcService` bean served by Spring gRPC |

## Implementation notes

Expand All @@ -37,7 +37,7 @@ The gRPC contract lives in `proto/` next to the services. The poms set the proto

Key variables (set by compose.yaml, override for local dev):

`spring.datasource.url, spring.kafka.bootstrap-servers, inventory.grpc.host`
`spring.datasource.url, spring.kafka.bootstrap-servers, spring.grpc.client.channel.inventory.target` (from `INVENTORY_ADDR`)

## Local development (without containers)

Expand Down
18 changes: 1 addition & 17 deletions examples/02-communications/spring-boot/inventory-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,7 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<artifactId>spring-boot-starter-grpc-server</artifactId>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.cndp.inventory;

import java.util.concurrent.ConcurrentHashMap;

import com.cndp.proto.InventoryGrpc;
import com.cndp.proto.ReserveReply;
import com.cndp.proto.ReserveRequest;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.grpc.server.service.GrpcService;

@GrpcService
public class InventoryService extends InventoryGrpc.InventoryImplBase {

private static final Logger log = LoggerFactory.getLogger(InventoryService.class);

private final ConcurrentHashMap<String, Integer> stock = new ConcurrentHashMap<>();
private final int initialStock;

public InventoryService(@Value("${inventory.initial-stock}") int initialStock) {
this.initialStock = initialStock;
}

@Override
public void reserveStock(ReserveRequest request, StreamObserver<ReserveReply> responseObserver) {
String sku = request.getSku();
int qty = request.getQuantity();

stock.putIfAbsent(sku, initialStock);

int remaining = stock.get(sku) - qty;
if (remaining >= 0) {
stock.put(sku, remaining);
log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining);
responseObserver.onNext(ReserveReply.newBuilder()
.setReserved(true).setRemaining(remaining).build());
} else {
log.warn("insufficient stock sku={} requested={} available={}", sku, qty, stock.get(sku));
responseObserver.onNext(ReserveReply.newBuilder()
.setReserved(false).setRemaining(stock.get(sku)).build());
}
responseObserver.onCompleted();
}
}
Original file line number Diff line number Diff line change
@@ -1,73 +1,13 @@
package com.cndp.inventory;

import java.util.concurrent.ConcurrentHashMap;

import com.cndp.proto.InventoryGrpc;
import com.cndp.proto.ReserveReply;
import com.cndp.proto.ReserveRequest;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/** Spring gRPC starts the gRPC server (spring.grpc.server.port) and registers every @GrpcService bean. */
@SpringBootApplication
public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase implements CommandLineRunner {

private static final Logger log = LoggerFactory.getLogger(InventoryServiceApplication.class);

private final ConcurrentHashMap<String, Integer> stock = new ConcurrentHashMap<>();

@Value("${inventory.initial-stock}")
private int initialStock;

@Value("${grpc.port}")
private int grpcPort;

private Server server;
public class InventoryServiceApplication {

public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}

@Override
public void run(String... args) throws Exception {
server = ServerBuilder.forPort(grpcPort)
.addService(this)
.build()
.start();
log.info("inventory gRPC server listening on :{}", grpcPort);
server.awaitTermination();
}

@PreDestroy
public void stopGrpcServer() {
if (server != null) server.shutdown();
}

@Override
public void reserveStock(ReserveRequest request, StreamObserver<ReserveReply> responseObserver) {
String sku = request.getSku();
int qty = request.getQuantity();

stock.putIfAbsent(sku, initialStock);

int remaining = stock.get(sku) - qty;
if (remaining >= 0) {
stock.put(sku, remaining);
log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining);
responseObserver.onNext(ReserveReply.newBuilder()
.setReserved(true).setRemaining(remaining).build());
} else {
log.warn("insufficient stock sku={} requested={} available={}", sku, qty, stock.get(sku));
responseObserver.onNext(ReserveReply.newBuilder()
.setReserved(false).setRemaining(stock.get(sku)).build());
}
responseObserver.onCompleted();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ spring:
main:
web-application-type: none

grpc:
port: ${GRPC_PORT:50051}
# Spring gRPC: the server port; the standard health and reflection services are on by default.
spring.grpc.server.port: ${GRPC_PORT:50051}

inventory:
initial-stock: ${INITIAL_STOCK:100}
Loading
Loading