agentsclimarketplace

Grpc patterns

Skill taipt1504/agent-skills/skills/grpc-patterns

gRPC patterns Java Spring Boot. CONDITIONAL — loads only if project has grpc-spring-boot-starter in build.gradle.From its SKILL.md

Install
npx -y skills add taipt1504/agent-skills --skill grpc-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.7 KB, as published. Nobody here has run it

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);
    }
}

// application.yml
// grpc.client.order-service.address=static://order-service:9090
// grpc.client.order-service.negotiation-type=plaintext

Error Handling

// Map domain exceptions to gRPC Status codes
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");
        // ⚠️ Never expose internal details to client
    };
}

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

// Server-side auth interceptor
@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 StatusHTTP EquivalentUse When
OK200Success
INVALID_ARGUMENT400Bad input
UNAUTHENTICATED401No/invalid auth
PERMISSION_DENIED403Forbidden
NOT_FOUND404Resource missing
ALREADY_EXISTS409Duplicate
RESOURCE_EXHAUSTED429Rate limited
INTERNAL500Server error (never expose details)
UNAVAILABLE503Downstream down
DEADLINE_EXCEEDED504Timeout

References

Keep looking

Skills are one crate of 626,194. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.