| name | grpc-patterns |
| description | gRPC patterns Java Spring Boot. CONDITIONAL — loads only if project has grpc-spring-boot-starter in build.gradle. |
| triggers | {"natural":["grpc","protobuf","rpc service","grpc streaming"],"code":["@GrpcService","@GrpcClient",".proto","StreamObserver","ManagedChannel"]} |
| applicability | {"always":false,"triggers":{"project_profile":["grpc:true"],"files_match":["**/*.proto","**/*GrpcService*.java","**/*GrpcClient*.java"],"code_patterns":["@GrpcService","@GrpcClient","StreamObserver","grpc.netty"],"task_keywords":["gRPC","protobuf","stream","bidirectional"],"related_rules":["rules/java/api-design.md","rules/java/security.md"]}} |
| relevance_assessment | HIGH 100%: project has grpc dep (verified grpc-spring-boot-starter in build.gradle) AND code touches @GrpcService/@GrpcClient/.proto
MEDIUM 40-79%: project has grpc dep, tangential touch
ZERO: no grpc dep (verify: grep grpc-spring-boot build.gradle = 0 results) — skip entirely
|
gRPC Patterns for Spring Boot
gRPC patterns for Java 17+ / Spring Boot 3.x.
Dependencies
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.salesforce.servicelibs</groupId>
<artifactId>reactor-grpc-stub</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<scope>test</scope>
</dependency>
Proto Design Essentials
syntax = "proto3";
package com.example.order.v1;
option java_multiple_files = true;
option java_package = "com.example.order.v1";
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse); // server streaming
rpc StreamOrders(stream OrderEvent) returns (stream OrderResponse); // bidirectional
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
string idempotency_key = 3; // always include for mutations
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
google.protobuf.Timestamp created_at = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // always define 0 as UNSPECIFIED
ORDER_STATUS_CREATED = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_CANCELLED = 3;
}
Server Implementation
@GrpcService
@RequiredArgsConstructor
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
private final OrderService orderService;
@Override
public void createOrder(CreateOrderRequest request,
StreamObserver<CreateOrderResponse> responseObserver) {
try {
var command = toCommand(request);
var result = orderService.create(command);
responseObserver.onNext(toProto(result));
responseObserver.onCompleted();
} catch (ValidationException e) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription(e.getMessage()).asRuntimeException());
} catch (NotFoundException e) {
responseObserver.onError(Status.NOT_FOUND
.withDescription(e.getMessage()).asRuntimeException());
} catch (Exception e) {
responseObserver.onError(Status.INTERNAL
.withDescription("Internal error").asRuntimeException());
}
}
@Override
public void listOrders(ListOrdersRequest request,
StreamObserver<OrderResponse> responseObserver) {
orderService.findByCustomer(request.getCustomerId())
.forEach(order -> responseObserver.onNext(toProto(order)));
responseObserver.onCompleted();
}
}
Client Implementation
@Configuration
public class GrpcClientConfig {
@Bean
public OrderServiceGrpc.OrderServiceBlockingStub orderBlockingStub(
@GrpcClient("order-service") ManagedChannel channel) {
return OrderServiceGrpc.newBlockingStub(channel);
}
}
Error Handling
private Status toStatus(Throwable e) {
return switch (e) {
case NotFoundException ex -> Status.NOT_FOUND.withDescription(ex.getMessage());
case ValidationException ex -> Status.INVALID_ARGUMENT.withDescription(ex.getMessage());
case UnauthorizedException ex -> Status.UNAUTHENTICATED.withDescription(ex.getMessage());
default -> Status.INTERNAL.withDescription("Internal server error");
};
}
Reactive gRPC (reactor-grpc)
@GrpcService
public class ReactiveOrderService extends ReactorOrderServiceGrpc.OrderServiceImplBase {
@Override
public Mono<CreateOrderResponse> createOrder(Mono<CreateOrderRequest> request) {
return request
.map(req -> toCommand(req))
.flatMap(orderService::create)
.map(order -> CreateOrderResponse.newBuilder()
.setOrderId(order.getId())
.setStatus(OrderStatus.ORDER_STATUS_CREATED)
.build());
}
@Override
public Flux<OrderResponse> listOrders(Mono<ListOrdersRequest> request) {
return request
.flatMapMany(req -> orderService.findByCustomer(req.getCustomerId()))
.map(this::toProto);
}
}
Interceptors
@Component
public class AuthInterceptor implements ServerInterceptor {
@Override
public <R, S> ServerCall.Listener<R> interceptCall(
ServerCall<R, S> call, Metadata headers, ServerCallHandler<R, S> next) {
String token = headers.get(AUTHORIZATION_METADATA_KEY);
if (!validateToken(token)) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata());
return new ServerCall.Listener<>() {};
}
Context ctx = Context.current().withValue(USER_ID_CTX_KEY, extractUserId(token));
return Contexts.interceptCall(ctx, call, headers, next);
}
}
Testing
@SpringBootTest
class OrderGrpcServiceTest {
private static Server server;
private static ManagedChannel channel;
@BeforeAll
static void setup() throws IOException {
OrderGrpcService service = new OrderGrpcService(mockOrderService());
server = InProcessServerBuilder.forName("test")
.directExecutor().addService(service).build().start();
channel = InProcessChannelBuilder.forName("test")
.directExecutor().build();
}
@Test
void shouldCreateOrder() {
var stub = OrderServiceGrpc.newBlockingStub(channel);
var request = CreateOrderRequest.newBuilder()
.setCustomerId("customer-1")
.addItems(OrderItem.newBuilder().setProductId("P1").setQuantity(2).build())
.build();
var response = stub.createOrder(request);
assertThat(response.getStatus()).isEqualTo(OrderStatus.ORDER_STATUS_CREATED);
}
}
Configuration (application.yml)
grpc:
server:
port: 9090
security:
certificateChain: classpath:tls/server.crt
privateKey: classpath:tls/server.key
client:
order-service:
address: static://order-service:9090
negotiation-type: tls
keepAliveTime: 30s
keepAliveTimeout: 5s
Status Code Mapping
| gRPC Status | HTTP Equivalent | Use When |
|---|
OK | 200 | Success |
INVALID_ARGUMENT | 400 | Bad input |
UNAUTHENTICATED | 401 | No/invalid auth |
PERMISSION_DENIED | 403 | Forbidden |
NOT_FOUND | 404 | Resource missing |
ALREADY_EXISTS | 409 | Duplicate |
RESOURCE_EXHAUSTED | 429 | Rate limited |
INTERNAL | 500 | Server error (never expose details) |
UNAVAILABLE | 503 | Downstream down |
DEADLINE_EXCEEDED | 504 | Timeout |
References
- references/proto-streaming.md — Proto design rules, backward compatibility, field numbering, all 4 streaming patterns
- references/reactive-advanced.md — reactor-grpc examples, deadline/timeout, load balancing, health checks
- references/security-interceptors.md — TLS, JWT auth interceptor, Context propagation, client-side interceptors
- references/testing-monitoring.md — InProcessServer tests, mock stubs, Micrometer gRPC metrics, anti-patterns