217 KiB
타입 안전 gRPC 실행 플랫폼 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Spring Boot 4.1을 실행 엔진으로 사용하면서 Protobuf 계약 거버넌스, Named Channel·Server Profile, 세 축 실행 증거, deadline·retry·idempotency, 안정 Status·Rich Error, DNS·Kubernetes discovery, bounded Server Streaming과 실제 Netty 검증을 제공하는 Stable 타입 안전 gRPC 실행 플랫폼을 구축한다.
Architecture: .proto와 generated Java contract는 grpc-proto-contract·grpc-codegen이 소유하고, 일반 애플리케이션은 Generated Stub과 Typed Service Adapter를 통해서만 RPC를 사용한다. grpc-core-api와 grpc-policy가 method policy·execution evidence·deadline·retry·metadata·streaming 계약을 정의하며, 상태 변경 RPC의 강한 완료 증거는 JPA Operation Ledger와 Application transaction의 결합으로 제공한다. Stable 범위는 Unary와 Server Streaming이며 Client/Bidi Streaming, xDS, gRPC-Web, Hedging과 언어별 adapter는 Advanced 계획으로 격리한다.
Tech Stack: Java 21, Gradle Kotlin DSL, Spring Boot 4.1 BOM, Boot-managed Spring gRPC, gRPC Java, Protobuf Java, Protobuf Common Protos, Netty and Netty Shaded transports, Buf CLI/Gradle plugin, Protovalidate, Spring Security, Spring Data JPA, Flyway, Micrometer Observation, OpenTelemetry bridge, JUnit 5, AssertJ, ArchUnit, Testcontainers, Toxiproxy.
Global Constraints
- Java runtime은
21이다. - Dependency version의 Source of Truth는 Spring Boot
4.1BOM이다. - Spring gRPC, gRPC Java, Protobuf Java, Common Protos 버전을 개별 모듈에서 임의로 override하지 않는다.
- Stable 신규 Schema는
proto3 + explicit optional을 사용한다. - Edition 2024는 Advanced opt-in이고 Edition 2026은 Experimental Watch다.
- Boot-managed Protobuf plugin이 Java code generation의 단일 owner다.
- Buf는 format, lint, breaking, descriptor/image와 schema artifact를 소유한다.
- Stable public API breaking gate는 Buf
FILEcategory다. - 모든 Stable Unary method는 positive deadline profile을 가져야 한다.
- gRPC Status와 업무 Commit evidence를 동일한 상태로 취급하지 않는다.
- 실행 증거는 Transport, Business, Stream 세 축으로 보존한다.
- Response Headers 관측을 Business Commit으로 승격하지 않는다.
DEADLINE_EXCEEDEDmutation은 완료 실패가 아니라 Completion Unknown일 수 있다.UNAVAILABLE만으로 상태 변경 RPC를 재호출하지 않는다.- explicit retry owner는 Application, gRPC Platform, Service Mesh 중 하나다.
- non-idempotent method에 explicit retry와 hedging을 허용하지 않는다.
- 상태 변경 RPC에 exactly-once transport 보장을 선언하지 않는다.
IDEMPOTENCY_KEY_REQUIREDmethod는 server-side Operation Ledger와 request fingerprint를 사용한다.- Operation Ledger와 business mutation은 가능한 경우 동일 DB transaction에 commit한다.
- Stable RPC 유형은 Unary와 Server Streaming이다.
- Client Streaming과 Bidirectional Streaming은 Advanced 계획에서 구현한다.
- Server Streaming writer는 bounded queue와 single serialized writer를 사용한다.
- StreamObserver에 여러 thread가 직접
onNext()하지 않는다. - partial stream delivery 이후 whole-call transparent retry를 수행하지 않는다.
- GraphQL/WebSocket/Messaging/Fileserver/Object Storage의 책임을 gRPC에서 중복 구현하지 않는다.
- 대형 binary는 Fileserver/Object Storage reference로 전달한다.
- 일반 Application source에서 raw gRPC builder·call API import를 금지한다.
- Netty가 Stable certification transport다.
- In-process transport는 fast contract test용이며 network/TLS evidence가 아니다.
- dev·stage·prod는 TLS를 요구하고 trust-all·hostname verification off를 금지한다.
- production Reflection은 기본 비활성이다.
- Stable resolver는 Static·DNS, Stable LB는 pick_first·round_robin이다.
- xDS와 custom resolver/LB는 Advanced다.
- Metric tag에 raw metadata, payload, actor/user/tenant/object/stream/idempotency ID를 넣지 않는다.
- Stable module root는
modules/grpc이다. - Root package는
io.backend.skeleton.grpc이다. - 모든 task는 red-green TDD와 독립 commit으로 끝난다.
- 실제 저장소 구조가 예상 경로와 다르면 파일 경로만 매핑하고 공개 계약·불변 조건·테스트 의미는 변경하지 않는다.
Execution Baseline
Stable Task 1–53
→ Stable Release Gate
→ Advanced Task 1–18
Stable Module Map
modules/grpc/
├── grpc-core-api
├── grpc-proto-contract
├── grpc-codegen
├── grpc-client
├── grpc-server
├── grpc-policy
├── grpc-discovery
├── grpc-admin
├── grpc-observability
├── grpc-operation-ledger-jpa
├── grpc-spring-boot-starter
├── grpc-testkit-core
├── grpc-testkit-inprocess
├── grpc-testkit-netty
└── grpc-testkit-fault
File Ownership Rules
grpc-core-api
→ identifiers, method policy, evidence, failure, request context, deadline primitive
grpc-proto-contract
→ public/common .proto and schema source
grpc-codegen
→ Gradle convention, Buf governance, descriptor and consumer fixture
grpc-client
→ named channel, runtime generation, typed stub, client metadata
grpc-server
→ service adapter, interceptor order, Netty profiles, architecture boundary
grpc-policy
→ validation, deadline, cancellation, security, status, retry,
idempotency, streaming, size and compression
grpc-discovery
→ stable resolver, load-balancing and Kubernetes profile
grpc-admin
→ health, reflection, drain and safe runtime snapshot
grpc-observability
→ bounded metric/trace/logging convention
grpc-operation-ledger-jpa
→ durable mutation idempotency and completion evidence
grpc-spring-boot-starter
→ auto-configuration, properties and startup validation
grpc-testkit-*
→ in-process, real Netty, fault, reliability and performance certification
Delivery Phases
| Phase | Tasks | Independently testable result |
|---|---|---|
| Foundation | 1–7 | Stable modules, identifiers, policy, evidence, failure, deadline, context |
| Contract Governance | 8–11 | Proto rules, Buf, Boot codegen, descriptor/consumer gate |
| Server Boundary | 12–23 | Validation, architecture, adapter, interceptor, server, security, health, reflection, drain |
| Client Runtime | 24–30 | Named Channel, generation, typed stub, metadata, deadline, cancellation, retry owner |
| Reliability | 31–33 | Retry eligibility, operation ledger, idempotency |
| Recovery·Discovery | 34–36 | Completion query, DNS/LB, Kubernetes profiles |
| Server Streaming | 37–43 | Envelope, writer, flow control, resume, lifetime, wait-for-ready, payload |
| Operations·Verification | 44–53 | Observability, admin, in-process, Netty, fault, reliability, performance, starter, release gate |
Task 1: Gradle 멀티모듈과 Stable 테스트 집계
Files:
- Modify:
settings.gradle.kts - Modify:
build.gradle.kts - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalog.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableBuildInvariant.java - Create:
modules/grpc/build.gradle.kts - Create:
modules/grpc/grpc-core-api/build.gradle.kts - Create:
modules/grpc/grpc-proto-contract/build.gradle.kts - Create:
modules/grpc/grpc-codegen/build.gradle.kts - Create:
modules/grpc/grpc-client/build.gradle.kts - Create:
modules/grpc/grpc-server/build.gradle.kts - Create:
modules/grpc/grpc-policy/build.gradle.kts - Create:
modules/grpc/grpc-discovery/build.gradle.kts - Create:
modules/grpc/grpc-admin/build.gradle.kts - Create:
modules/grpc/grpc-observability/build.gradle.kts - Create:
modules/grpc/grpc-operation-ledger-jpa/build.gradle.kts - Create:
modules/grpc/grpc-spring-boot-starter/build.gradle.kts - Create:
modules/grpc/grpc-testkit-core/build.gradle.kts - Create:
modules/grpc/grpc-testkit-inprocess/build.gradle.kts - Create:
modules/grpc/grpc-testkit-netty/build.gradle.kts - Create:
modules/grpc/grpc-testkit-fault/build.gradle.kts - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalogTest.java
Interfaces:
- Consumes: Root Gradle settings, version catalog와 Spring Boot 4.1 BOM.
- Produces:
modules/grpc의 Stable module 목록, 금지 dependency 규칙,grpcStableTestaggregate task.
Implementation requirements:
-
Stable module은
grpc-core-api,grpc-proto-contract,grpc-codegen,grpc-client,grpc-server,grpc-policy,grpc-discovery,grpc-admin,grpc-observability,grpc-operation-ledger-jpa,grpc-spring-boot-starter,grpc-testkit-core,grpc-testkit-inprocess,grpc-testkit-netty,grpc-testkit-fault로 고정한다. -
모든 runtime dependency version은 Spring Boot 4.1 BOM을 사용한다.
-
grpc-proto-contract와grpc-core-api는 Spring Boot·Netty runtime에 의존하지 않는다. -
Stable starter가
modules/grpc-advanced를 참조하면 build를 실패시킨다. -
grpcStableTest,grpcContractTest,grpcNettyTest,grpcFaultTest,grpcPerformanceTesttask를 등록한다. -
Step 1: Write the failing test
class GrpcStableModuleCatalogTest {
@org.junit.jupiter.api.Test
void stableModuleSetIsExactAndAdvancedIsExcluded() {
var catalog = GrpcStableModuleCatalog.standard();
org.assertj.core.api.Assertions.assertThat(catalog.modules())
.contains("grpc-core-api", "grpc-client", "grpc-server")
.doesNotContain("grpc-xds", "grpc-web");
org.assertj.core.api.Assertions.assertThat(
GrpcStableBuildInvariant.advancedDependencyAllowed())
.isFalse();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcStableModuleCatalogTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcStableModuleCatalog(
java.util.Set<String> modules) {
public static GrpcStableModuleCatalog standard() {
return new GrpcStableModuleCatalog(java.util.Set.of(
"grpc-core-api", "grpc-proto-contract", "grpc-codegen",
"grpc-client", "grpc-server", "grpc-policy",
"grpc-discovery", "grpc-admin", "grpc-observability",
"grpc-operation-ledger-jpa", "grpc-spring-boot-starter",
"grpc-testkit-core", "grpc-testkit-inprocess",
"grpc-testkit-netty", "grpc-testkit-fault"));
}
}
public final class GrpcStableBuildInvariant {
public static boolean advancedDependencyAllowed() {
return false;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcStableModuleCatalogTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalog.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcStableBuildInvariant.java' 'modules/grpc/build.gradle.kts' 'modules/grpc/grpc-core-api/build.gradle.kts' 'modules/grpc/grpc-proto-contract/build.gradle.kts' 'modules/grpc/grpc-codegen/build.gradle.kts' 'modules/grpc/grpc-client/build.gradle.kts' 'modules/grpc/grpc-server/build.gradle.kts' 'modules/grpc/grpc-policy/build.gradle.kts' 'modules/grpc/grpc-discovery/build.gradle.kts' 'modules/grpc/grpc-admin/build.gradle.kts' 'modules/grpc/grpc-observability/build.gradle.kts' 'modules/grpc/grpc-operation-ledger-jpa/build.gradle.kts' 'modules/grpc/grpc-spring-boot-starter/build.gradle.kts' 'modules/grpc/grpc-testkit-core/build.gradle.kts' 'modules/grpc/grpc-testkit-inprocess/build.gradle.kts' 'modules/grpc/grpc-testkit-netty/build.gradle.kts' 'modules/grpc/grpc-testkit-fault/build.gradle.kts' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcStableModuleCatalogTest.java' 'settings.gradle.kts' 'build.gradle.kts'
git commit -m "build: establish grpc stable module graph"
Task 2: Core 식별자와 RPC 유형
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcMethodName.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcServiceName.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcChannelProfileName.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/RpcType.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcCoreIdentifiersTest.java
Interfaces:
- Consumes: Task 1의 Stable module graph.
- Produces: 공백·불완전 이름을 거부하는 bounded identifier와 Unary·Streaming RPC 유형.
Implementation requirements:
-
GrpcMethodName은 canonical full method name인package.Service/Method형식을 요구한다. -
GrpcServiceName과GrpcChannelProfileName은 빈 값과 제어 문자를 거부한다. -
RpcType은UNARY,SERVER_STREAMING,CLIENT_STREAMING,BIDI_STREAMING만 가진다. -
식별자 원문은 metric label에 자동 사용하지 않는다.
-
generated descriptor와 catalog가 동일 canonical name을 사용한다.
-
Step 1: Write the failing test
class GrpcCoreIdentifiersTest {
@org.junit.jupiter.api.Test
void methodNameRequiresServiceAndMethod() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcMethodName("DocumentService"))
.isInstanceOf(IllegalArgumentException.class);
org.assertj.core.api.Assertions.assertThat(
new GrpcMethodName(
"hyeonworks.document.v1.DocumentService/GetDocument")
.value())
.endsWith("/GetDocument");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcCoreIdentifiersTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcMethodName(String value) {
public GrpcMethodName {
if (value == null
|| !value.matches("[A-Za-z0-9_.]+/[A-Za-z0-9_]+")) {
throw new IllegalArgumentException(
"full gRPC method name is required");
}
}
}
public record GrpcServiceName(String value) {
public GrpcServiceName {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("service name");
}
}
}
public enum RpcType {
UNARY, SERVER_STREAMING, CLIENT_STREAMING, BIDI_STREAMING
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.core.GrpcCoreIdentifiersTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcMethodName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcServiceName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/GrpcChannelProfileName.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/core/RpcType.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/core/GrpcCoreIdentifiersTest.java'
git commit -m "feat: add grpc core identifiers"
Task 3: Method Policy와 Idempotency Profile
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/RpcIdempotencyProfile.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicy.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalog.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/WaitForReadyPolicy.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalogTest.java
Interfaces:
- Consumes: Task 2의 canonical method name과 RPC 유형.
- Produces: method별 deadline·retry·idempotency·size·metadata 정책을 조회하는 immutable catalog.
Implementation requirements:
-
Idempotency profile은
READ_ONLY,NATURALLY_IDEMPOTENT,IDEMPOTENCY_KEY_REQUIRED,CONDITIONALLY_IDEMPOTENT,NON_IDEMPOTENT,STREAMING이다. -
Stable Unary method에는 deadline profile이 반드시 존재한다.
-
NON_IDEMPOTENTmethod에 explicit retry profile을 연결하면 catalog build를 실패시킨다. -
STREAMINGprofile은 whole-call retry profile을 허용하지 않는다. -
중복 method registration과 descriptor에 없는 method registration을 거부한다.
-
Step 1: Write the failing test
class GrpcMethodPolicyCatalogTest {
@org.junit.jupiter.api.Test
void nonIdempotentMethodCannotEnableExplicitRetry() {
var policy = GrpcMethodPolicy.nonIdempotentUnary(
new GrpcMethodName("x.y.CommandService/Create"),
"user-sync",
"retry-write");
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> GrpcMethodPolicyCatalog.of(java.util.List.of(policy)))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.policy.GrpcMethodPolicyCatalogTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum RpcIdempotencyProfile {
READ_ONLY,
NATURALLY_IDEMPOTENT,
IDEMPOTENCY_KEY_REQUIRED,
CONDITIONALLY_IDEMPOTENT,
NON_IDEMPOTENT,
STREAMING
}
public record GrpcMethodPolicy(
GrpcMethodName method,
RpcType rpcType,
RpcIdempotencyProfile idempotency,
String deadlineProfile,
String retryProfile,
WaitForReadyPolicy waitForReady) {
public static GrpcMethodPolicy nonIdempotentUnary(
GrpcMethodName method,
String deadline,
String retry) {
return new GrpcMethodPolicy(method, RpcType.UNARY,
RpcIdempotencyProfile.NON_IDEMPOTENT,
deadline, retry, WaitForReadyPolicy.DISABLED);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.policy.GrpcMethodPolicyCatalogTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/RpcIdempotencyProfile.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicy.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalog.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/policy/WaitForReadyPolicy.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/policy/GrpcMethodPolicyCatalogTest.java'
git commit -m "feat: define grpc method policy catalog"
Task 4: 세 축 실행 증거 모델
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcTransportEvidence.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcBusinessEvidence.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcStreamEvidence.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidence.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidenceTest.java
Interfaces:
- Consumes: Task 2의 RPC 유형과 method identity.
- Produces: Transport·Business·Stream evidence를 독립적으로 보존하는 immutable snapshot.
Implementation requirements:
-
RESPONSE_HEADERS_SEEN을COMMIT_CONFIRMED로 자동 승격하지 않는다. -
Stream evidence는
None,Partial,Applied,Resumablesealed hierarchy다. -
APPLIED는 application acknowledgement 증거가 있을 때만 생성한다. -
Unary RPC에서 non-none stream evidence를 설정하면 검증 오류다.
-
Evidence snapshot은 예외와 관측 event에서 동일 타입을 사용한다.
-
Step 1: Write the failing test
class GrpcExecutionEvidenceTest {
@org.junit.jupiter.api.Test
void responseHeadersDoNotProveBusinessCommit() {
var evidence = new GrpcExecutionEvidence(
GrpcTransportEvidence.RESPONSE_HEADERS_SEEN,
GrpcBusinessEvidence.UNKNOWN,
new GrpcStreamEvidence.None());
org.assertj.core.api.Assertions.assertThat(evidence.business())
.isEqualTo(GrpcBusinessEvidence.UNKNOWN);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.evidence.GrpcExecutionEvidenceTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcTransportEvidence {
NOT_SENT,
MAY_HAVE_LEFT_CLIENT,
RESPONSE_HEADERS_SEEN,
RESPONSE_MESSAGE_SEEN,
TRAILERS_SEEN
}
public enum GrpcBusinessEvidence {
NOT_OBSERVED,
APPLICATION_STARTED,
COMMIT_CONFIRMED,
ABORT_CONFIRMED,
UNKNOWN
}
public sealed interface GrpcStreamEvidence {
record None() implements GrpcStreamEvidence {}
record Partial(long lastSequence) implements GrpcStreamEvidence {}
record Applied(long lastAppliedSequence)
implements GrpcStreamEvidence {}
record Resumable(String snapshotVersion, String cursor)
implements GrpcStreamEvidence {}
}
public record GrpcExecutionEvidence(
GrpcTransportEvidence transport,
GrpcBusinessEvidence business,
GrpcStreamEvidence stream) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.evidence.GrpcExecutionEvidenceTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcTransportEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcBusinessEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcStreamEvidence.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidence.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/evidence/GrpcExecutionEvidenceTest.java'
git commit -m "feat: model grpc execution evidence"
Task 5: Stable 오류·Status·Completion Outcome
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureCategory.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcCompletionOutcome.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureContext.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcPlatformException.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/error/GrpcFailureContextTest.java
Interfaces:
- Consumes: Task 4의 실행 증거.
- Produces: gRPC Status와 업무 완료 불명확성을 분리하는 안정 오류 모델.
Implementation requirements:
-
GrpcCompletionOutcome은COMPLETED,REJECTED,COMPLETION_UNKNOWN,PARTIAL_STREAM을 표현한다. -
failure context에 method, status code, evidence, retry disposition, attempt, elapsed, trace ID를 보존한다.
-
payload, raw metadata, token, idempotency key 원문을 exception message에 넣지 않는다.
-
DEADLINE_EXCEEDEDmutation은 기본적으로COMPLETION_UNKNOWN후보다. -
provider·driver exception은 cause로 보존하되 공개 wire detail로 자동 변환하지 않는다.
-
Step 1: Write the failing test
class GrpcFailureContextTest {
@org.junit.jupiter.api.Test
void deadlineAfterPossibleSendIsCompletionUnknown() {
var context = GrpcFailureContext.deadlineExceeded(
new GrpcMethodName("x.y.CommandService/Create"),
GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT);
org.assertj.core.api.Assertions.assertThat(context.outcome())
.isEqualTo(GrpcCompletionOutcome.COMPLETION_UNKNOWN);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.error.GrpcFailureContextTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcCompletionOutcome {
COMPLETED, REJECTED, COMPLETION_UNKNOWN, PARTIAL_STREAM
}
public record GrpcFailureContext(
GrpcMethodName method,
String statusCode,
GrpcExecutionEvidence evidence,
GrpcCompletionOutcome outcome,
boolean retryable,
int attempt,
java.time.Duration elapsed,
String traceId) {
public static GrpcFailureContext deadlineExceeded(
GrpcMethodName method,
GrpcTransportEvidence transport) {
return new GrpcFailureContext(method, "DEADLINE_EXCEEDED",
new GrpcExecutionEvidence(transport,
GrpcBusinessEvidence.UNKNOWN,
new GrpcStreamEvidence.None()),
GrpcCompletionOutcome.COMPLETION_UNKNOWN,
false, 1, java.time.Duration.ZERO, "redacted");
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.error.GrpcFailureContextTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureCategory.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcCompletionOutcome.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcFailureContext.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/error/GrpcPlatformException.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/error/GrpcFailureContextTest.java'
git commit -m "feat: add grpc failure and completion model"
Task 6: Deadline·Cancellation Core Primitive
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudget.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationToken.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineProfile.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineExceededException.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudgetTest.java
Interfaces:
- Consumes: Task 3의 method policy.
- Produces: 상위 deadline과 method profile에서 안전 margin을 차감한 downstream budget.
Implementation requirements:
-
모든 Stable Unary method에 positive deadline을 요구한다.
-
downstream deadline은 parent remaining과 method default 중 더 짧은 값을 사용한다.
-
serialization/trailer safety reserve를 차감한다.
-
남은 시간이 minimum attempt budget보다 작으면 새 dependency call을 시작하지 않는다.
-
cancellation token은 cancel reason과 발생 시각을 보존하고 idempotent하게 취소된다.
-
Step 1: Write the failing test
class GrpcDeadlineBudgetTest {
@org.junit.jupiter.api.Test
void downstreamBudgetNeverExceedsParent() {
var parent = GrpcDeadlineBudget.of(
java.time.Duration.ofSeconds(2));
var downstream = parent.child(
java.time.Duration.ofSeconds(5),
java.time.Duration.ofMillis(100));
org.assertj.core.api.Assertions.assertThat(
downstream.remaining())
.isLessThanOrEqualTo(java.time.Duration.ofMillis(1900));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineBudgetTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcDeadlineBudget(
java.time.Instant deadline) {
public static GrpcDeadlineBudget of(
java.time.Duration duration) {
if (duration.isZero() || duration.isNegative()) {
throw new IllegalArgumentException("positive deadline");
}
return new GrpcDeadlineBudget(
java.time.Instant.now().plus(duration));
}
public java.time.Duration remaining() {
var value = java.time.Duration.between(
java.time.Instant.now(), deadline);
return value.isNegative() ? java.time.Duration.ZERO : value;
}
public GrpcDeadlineBudget child(
java.time.Duration configured,
java.time.Duration reserve) {
var child = java.time.Instant.now()
.plus(configured).minus(reserve);
return new GrpcDeadlineBudget(
child.isBefore(deadline) ? child : deadline.minus(reserve));
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineBudgetTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudget.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationToken.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineProfile.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineExceededException.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineBudgetTest.java'
git commit -m "feat: add grpc deadline and cancellation primitives"
Task 7: Metadata·Actor·Tenant Request Context
Files:
- Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcRequestContext.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataKey.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataBudget.java - Create:
modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcClientIdentity.java - Test:
modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/context/GrpcMetadataBudgetTest.java
Interfaces:
- Consumes: Task 6의 deadline budget과 기존 Security의 Actor·Tenant context.
- Produces: 허용된 metadata만 immutable request context로 변환하는 bounded contract.
Implementation requirements:
-
metadata key는 allowlist catalog에 등록된 ASCII/binary key만 허용한다.
-
grpc-prefix 사용자 정의를 거부한다. -
전체 hard budget과 user-defined soft budget을 분리한다.
-
Actor·Tenant는 검증된 authentication source에서만 생성하고 raw tenant header를 직접 신뢰하지 않는다.
-
credential, token, PII와 raw metadata map은 public request context에서 제거한다.
-
Step 1: Write the failing test
class GrpcMetadataBudgetTest {
@org.junit.jupiter.api.Test
void oversizedUserMetadataIsRejected() {
var budget = new GrpcMetadataBudget(8192, 4096);
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> budget.validate(2000, 5000))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.context.GrpcMetadataBudgetTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcMetadataBudget(
int hardBytes,
int userBytes) {
public void validate(int frameworkBytes, int suppliedUserBytes) {
if (suppliedUserBytes > userBytes
|| frameworkBytes + suppliedUserBytes > hardBytes) {
throw new IllegalArgumentException(
"gRPC metadata budget exceeded");
}
}
}
public record GrpcRequestContext(
Object actor,
Object tenant,
java.util.Locale locale,
String correlationId,
String traceId,
GrpcDeadlineBudget deadline,
GrpcClientIdentity client) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-core-api:test --tests 'io.backend.skeleton.grpc.context.GrpcMetadataBudgetTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcRequestContext.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataKey.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcMetadataBudget.java' 'modules/grpc/grpc-core-api/src/main/java/io/backend/skeleton/grpc/context/GrpcClientIdentity.java' 'modules/grpc/grpc-core-api/src/test/java/io/backend/skeleton/grpc/context/GrpcMetadataBudgetTest.java'
git commit -m "feat: define grpc metadata and request context"
Task 8: Proto Style Manifest와 Schema 규칙
Files:
- Create:
modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoStyleManifest.java - Create:
modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoRuleViolation.java - Create:
modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidator.java - Create:
modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/error.proto - Create:
modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/stream.proto - Create:
modules/grpc/grpc-proto-contract/src/main/proto/buf.yaml - Test:
modules/grpc/grpc-proto-contract/src/test/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidatorTest.java
Interfaces:
- Consumes: Task 2의 service·method identity와 source
.protofiles. - Produces: proto3+optional, package, reserved, enum zero, WKT 사용 규칙을 검증하는 contract validator.
Implementation requirements:
-
Stable source는
proto3이며 presence가 필요한 scalar는optional이다. -
package는
{organization}.{domain}.v{major}규칙을 따른다. -
java_multiple_files = true와 hand-written package와 분리된java_package를 요구한다. -
삭제된 field number/name의
reserved선언을 schema history와 비교한다. -
enum 0 값은
_UNSPECIFIEDsuffix를 요구하고Any,Struct, map 사용을 allowlist한다. -
Step 1: Write the failing test
class GrpcProtoContractValidatorTest {
@org.junit.jupiter.api.Test
void enumWithoutUnspecifiedZeroIsRejected() {
var manifest = GrpcProtoStyleManifest.stable();
var validator = new GrpcProtoContractValidator(manifest);
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> validator.validateEnumZero("Status", "ACTIVE", 0))
.isInstanceOf(GrpcProtoRuleViolation.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-proto-contract:test --tests 'io.backend.skeleton.grpc.contract.GrpcProtoContractValidatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcProtoStyleManifest(
String syntax,
boolean explicitPresence,
boolean javaMultipleFiles,
String enumZeroSuffix) {
public static GrpcProtoStyleManifest stable() {
return new GrpcProtoStyleManifest(
"proto3", true, true, "_UNSPECIFIED");
}
}
public final class GrpcProtoContractValidator {
private final GrpcProtoStyleManifest manifest;
public GrpcProtoContractValidator(
GrpcProtoStyleManifest manifest) {
this.manifest = manifest;
}
public void validateEnumZero(
String enumName, String valueName, int number) {
if (number == 0
&& !valueName.endsWith(
manifest.enumZeroSuffix())) {
throw new GrpcProtoRuleViolation(enumName);
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-proto-contract:test --tests 'io.backend.skeleton.grpc.contract.GrpcProtoContractValidatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoStyleManifest.java' 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoRuleViolation.java' 'modules/grpc/grpc-proto-contract/src/main/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidator.java' 'modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/error.proto' 'modules/grpc/grpc-proto-contract/src/main/proto/hyeonworks/grpc/common/v1/stream.proto' 'modules/grpc/grpc-proto-contract/src/main/proto/buf.yaml' 'modules/grpc/grpc-proto-contract/src/test/java/io/backend/skeleton/grpc/contract/GrpcProtoContractValidatorTest.java'
git commit -m "feat: enforce grpc proto style manifest"
Task 9: Buf Format·Lint·Breaking Governance
Files:
- Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicy.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBreakingCategory.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaBaseline.java - Create:
modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-buf-conventions.gradle.kts - Create:
modules/grpc/grpc-proto-contract/buf.lock - Create:
modules/grpc/grpc-proto-contract/buf.gen.yaml - Test:
modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicyTest.java
Interfaces:
- Consumes: Task 8의 Proto style manifest와 released schema artifact.
- Produces: Buf
FILEbreaking gate, lint·format·descriptor build를 Gradle lifecycle에 연결하는 convention.
Implementation requirements:
-
bufFormatCheck,bufLint,bufBuild,bufBreaking을 CI task로 등록한다. -
Stable public API는 Buf
FILEcategory를 사용한다. -
비교 baseline은 마지막 released schema artifact로 고정한다.
-
wire-only compatibility로 source-breaking 변경을 통과시키지 않는다.
-
breaking 결과와 schema hash를 release artifact에 보존한다.
-
Step 1: Write the failing test
class GrpcBufPolicyTest {
@org.junit.jupiter.api.Test
void stableUsesFileBreakingCategory() {
var policy = GrpcBufPolicy.stable(
new GrpcSchemaBaseline("1.0.0", "sha256:abc"));
org.assertj.core.api.Assertions.assertThat(policy.category())
.isEqualTo(GrpcBreakingCategory.FILE);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcBufPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcBreakingCategory {
FILE, PACKAGE, WIRE_JSON, WIRE
}
public record GrpcSchemaBaseline(
String version,
String digest) {}
public record GrpcBufPolicy(
GrpcBreakingCategory category,
GrpcSchemaBaseline baseline,
boolean formatRequired,
boolean lintRequired) {
public static GrpcBufPolicy stable(
GrpcSchemaBaseline baseline) {
return new GrpcBufPolicy(
GrpcBreakingCategory.FILE,
baseline, true, true);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcBufPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicy.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcBreakingCategory.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaBaseline.java' 'modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-buf-conventions.gradle.kts' 'modules/grpc/grpc-proto-contract/buf.lock' 'modules/grpc/grpc-proto-contract/buf.gen.yaml' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcBufPolicyTest.java'
git commit -m "build: add buf schema governance"
Task 10: Spring Boot BOM 기반 Protobuf·gRPC Java Codegen
Files:
- Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifest.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcGeneratedPackagePolicy.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenOutput.java - Create:
modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-codegen-conventions.gradle.kts - Create:
modules/grpc/grpc-codegen/src/main/resources/META-INF/gradle-plugins/io.backend.grpc-codegen-conventions.properties - Test:
modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifestTest.java
Interfaces:
- Consumes: Task 9의 Buf policy와 Spring Boot 4.1 dependency management.
- Produces: protoc·grpc-java generation option, output package, descriptor path를 고정하는 build convention.
Implementation requirements:
-
Protobuf와 gRPC Java plugin version을 Boot BOM에서 읽고 중복 version 선언을 금지한다.
-
generated source root는 build directory 아래에 둔다.
-
hand-written package와 generated
*.protopackage를 겹치게 하지 않는다. -
Java code generation owner는 Boot-managed protobuf plugin 한 곳이다.
-
descriptor set과 source info를 release artifact로 생성한다.
-
Step 1: Write the failing test
class GrpcCodegenManifestTest {
@org.junit.jupiter.api.Test
void bootBomOwnsToolVersions() {
var manifest = GrpcCodegenManifest.bootManaged();
org.assertj.core.api.Assertions.assertThat(
manifest.explicitProtocVersion()).isEmpty();
org.assertj.core.api.Assertions.assertThat(
manifest.descriptorSetEnabled()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcCodegenManifestTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcCodegenManifest(
java.util.Optional<String> explicitProtocVersion,
java.util.Optional<String> explicitGrpcPluginVersion,
boolean descriptorSetEnabled,
boolean includeSourceInfo) {
public static GrpcCodegenManifest bootManaged() {
return new GrpcCodegenManifest(
java.util.Optional.empty(),
java.util.Optional.empty(),
true, true);
}
}
public record GrpcGeneratedPackagePolicy(
String protoPackagePrefix,
String javaPackagePrefix,
boolean separateFromHandwritten) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcCodegenManifestTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifest.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcGeneratedPackagePolicy.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcCodegenOutput.java' 'modules/grpc/grpc-codegen/src/main/kotlin/io.backend.grpc-codegen-conventions.gradle.kts' 'modules/grpc/grpc-codegen/src/main/resources/META-INF/gradle-plugins/io.backend.grpc-codegen-conventions.properties' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcCodegenManifestTest.java'
git commit -m "build: standardize grpc java code generation"
Task 11: Descriptor Artifact와 Consumer Source 호환성
Files:
- Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifact.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcConsumerFixture.java - Create:
modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaArtifactPublisher.java - Create:
modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/build.gradle.kts - Create:
modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/src/main/java/fixture/DocumentClientFixture.java - Test:
modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifactTest.java
Interfaces:
- Consumes: Task 10의 generated descriptor와 Task 9의 schema baseline.
- Produces: schema hash·descriptor·generated source hash·consumer compile 결과를 하나의 release artifact로 묶는 계약.
Implementation requirements:
-
FileDescriptorSet, Buf image, schema hash, policy version을 보존한다.
-
이전 Java client fixture를 새 generated sources와 함께 compile한다.
-
service path, method path, Java package source break를 별도로 보고한다.
-
artifact는 immutable version으로 publish한다.
-
consumer fixture 실패는 release를 차단한다.
-
Step 1: Write the failing test
class GrpcDescriptorArtifactTest {
@org.junit.jupiter.api.Test
void artifactRequiresConsumerCompatibilityEvidence() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcDescriptorArtifact(
"sha256:schema", "sha256:generated",
java.util.List.of()))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcDescriptorArtifactTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcConsumerFixture(
String name,
boolean compiled) {}
public record GrpcDescriptorArtifact(
String schemaDigest,
String generatedSourceDigest,
java.util.List<GrpcConsumerFixture> fixtures) {
public GrpcDescriptorArtifact {
if (fixtures == null || fixtures.isEmpty()
|| fixtures.stream().anyMatch(
fixture -> !fixture.compiled())) {
throw new IllegalArgumentException(
"consumer compatibility evidence required");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-codegen:test --tests 'io.backend.skeleton.grpc.codegen.GrpcDescriptorArtifactTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifact.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcConsumerFixture.java' 'modules/grpc/grpc-codegen/src/main/java/io/backend/skeleton/grpc/codegen/GrpcSchemaArtifactPublisher.java' 'modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/build.gradle.kts' 'modules/grpc/grpc-codegen/src/test/resources/consumer-fixtures/v1/src/main/java/fixture/DocumentClientFixture.java' 'modules/grpc/grpc-codegen/src/test/java/io/backend/skeleton/grpc/codegen/GrpcDescriptorArtifactTest.java'
git commit -m "build: publish grpc schema compatibility artifact"
Task 12: Protovalidate Transport Validation
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcTransportValidator.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcValidationViolation.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptor.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptorTest.java
Interfaces:
- Consumes: Generated request messages, Task 8의 validation owner rule와 server interceptor SPI.
- Produces: pure transport constraint를 Application Use Case 진입 전에 검증하는 interceptor.
Implementation requirements:
-
길이, 범위, collection count, format, pure cross-field rule만 처리한다.
-
DB 조회·권한·현재 상태 검증은 금지한다.
-
violation을
INVALID_ARGUMENT와BadRequestdetail로 변환한다. -
request body와 민감 field 값을 로그에 기록하지 않는다.
-
validation rule compilation failure는 build 또는 startup failure다.
-
Step 1: Write the failing test
class ProtovalidateGrpcInterceptorTest {
@org.junit.jupiter.api.Test
void invalidRequestNeverReachesApplication() {
var validator = new GrpcTransportValidator(
value -> java.util.List.of(
new GrpcValidationViolation(
"title", "max_len", "too long")));
org.assertj.core.api.Assertions.assertThat(
validator.validate(new Object())).isNotEmpty();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.validation.ProtovalidateGrpcInterceptorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcValidationViolation(
String field,
String rule,
String safeMessage) {}
public final class GrpcTransportValidator {
private final java.util.function.Function<Object,
java.util.List<GrpcValidationViolation>> delegate;
public GrpcTransportValidator(
java.util.function.Function<Object,
java.util.List<GrpcValidationViolation>> delegate) {
this.delegate = delegate;
}
public java.util.List<GrpcValidationViolation> validate(
Object request) {
return java.util.List.copyOf(delegate.apply(request));
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.validation.ProtovalidateGrpcInterceptorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcTransportValidator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/GrpcValidationViolation.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptor.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/validation/ProtovalidateGrpcInterceptorTest.java'
git commit -m "feat: add grpc transport validation"
Task 13: Application 경계와 Raw gRPC API 차단
Files:
- Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcServiceAdapterMarker.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRules.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcRawApiImportRule.java - Test:
modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRulesTest.java
Interfaces:
- Consumes: Task 1 module graph, generated service classes와 Application package conventions.
- Produces: Service Adapter가 Use Case만 호출하고 DB·HTTP·Messaging·Storage에 직접 접근하지 못하게 하는 ArchUnit rule.
Implementation requirements:
-
gRPC service adapter는
applicationport만 의존한다. -
EntityManager, Repository,MongoTemplate, HTTP client, broker template, content store direct dependency를 금지한다. -
일반 application module에서 raw
ManagedChannelBuilder,ServerBuilder,ClientCall,MethodDescriptorimport를 금지한다. -
generated package는 raw API import rule 예외다.
-
architecture violation은 test failure다.
-
Step 1: Write the failing test
class GrpcApplicationBoundaryRulesTest {
@org.junit.jupiter.api.Test
void rawBuildersAreNotApplicationApi() {
var rule = GrpcRawApiImportRule.defaultRule();
org.assertj.core.api.Assertions.assertThat(
rule.forbiddenTypes())
.contains("io.grpc.ManagedChannelBuilder",
"io.grpc.ServerBuilder");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.architecture.GrpcApplicationBoundaryRulesTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public interface GrpcServiceAdapterMarker {}
public record GrpcRawApiImportRule(
java.util.Set<String> forbiddenTypes) {
public static GrpcRawApiImportRule defaultRule() {
return new GrpcRawApiImportRule(java.util.Set.of(
"io.grpc.ManagedChannelBuilder",
"io.grpc.ServerBuilder",
"io.grpc.MethodDescriptor",
"io.grpc.ClientCall"));
}
}
public final class GrpcApplicationBoundaryRules {
public static java.util.Set<String> forbiddenDependencies() {
return java.util.Set.of(
"jakarta.persistence.EntityManager",
"org.springframework.data.repository.Repository",
"org.springframework.data.mongodb.core.MongoTemplate",
"org.springframework.web.client.RestClient",
"org.springframework.web.reactive.function.client.WebClient");
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.architecture.GrpcApplicationBoundaryRulesTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcServiceAdapterMarker.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRules.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/architecture/GrpcRawApiImportRule.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/architecture/GrpcApplicationBoundaryRulesTest.java'
git commit -m "test: enforce grpc application boundary"
Task 14: Typed Service Adapter SPI
Files:
- Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapter.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcApplicationInvocation.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcResponseMapper.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterDescriptor.java - Test:
modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterTest.java
Interfaces:
- Consumes: Task 12 architecture boundary, generated request/response와 Application Use Case port.
- Produces: Proto ↔ Application model 변환과 Use Case 호출만 수행하는 typed adapter contract.
Implementation requirements:
-
adapter는 request mapper, application invocation, response mapper를 명시한다.
-
Application invocation에 immutable
GrpcRequestContext를 전달한다. -
adapter가 transport Status를 직접 임의 구성하지 않고 공통 error mapper를 사용한다.
-
blocking·async 실행 유형을 descriptor에 선언한다.
-
파일 bytes와 provider SDK 객체를 request/response mapping에 노출하지 않는다.
-
Step 1: Write the failing test
class GrpcServiceAdapterTest {
@org.junit.jupiter.api.Test
void adapterDelegatesToApplicationInvocation() {
var invoked = new java.util.concurrent.atomic.AtomicBoolean();
GrpcApplicationInvocation<String, String> invocation =
(command, context) -> {
invoked.set(true);
return "ok";
};
org.assertj.core.api.Assertions.assertThat(
invocation.invoke("command", null)).isEqualTo("ok");
org.assertj.core.api.Assertions.assertThat(invoked).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServiceAdapterTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
@FunctionalInterface
public interface GrpcApplicationInvocation<C, R> {
R invoke(C command, GrpcRequestContext context);
}
public interface GrpcServiceAdapter<P, C, R, Q>
extends GrpcServiceAdapterMarker {
C toCommand(P request);
R invoke(C command, GrpcRequestContext context);
Q toResponse(R result);
GrpcServiceAdapterDescriptor descriptor();
}
public record GrpcServiceAdapterDescriptor(
GrpcMethodName method,
String executionProfile,
String responseProfile) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServiceAdapterTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapter.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcApplicationInvocation.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcResponseMapper.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterDescriptor.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServiceAdapterTest.java'
git commit -m "feat: add typed grpc service adapter"
Task 15: Server Interceptor 순서 계약
Files:
- Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorStage.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrder.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorChain.java - Test:
modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrderTest.java
Interfaces:
- Consumes: Task 11 validation, Task 13 service adapter와 Security·Observability SPI.
- Produces: 예외 경계부터 validation과 service adapter까지의 의미 순서를 고정하는 chain.
Implementation requirements:
-
순서는 Exception Boundary → Trace → Authentication → Actor/Tenant → Authorization → Admission → Deadline/Cancellation → Idempotency → Validation → Service Adapter다.
-
중복 stage와 누락 required stage를 startup에서 거부한다.
-
실제 framework wrapping 방향을 integration test로 검증한다.
-
Exception mapper가 downstream 전체를 감싼다.
-
client interceptor와 server interceptor 순서를 별도 catalog로 관리한다.
-
Step 1: Write the failing test
class GrpcServerInterceptorOrderTest {
@org.junit.jupiter.api.Test
void stableOrderPlacesValidationAfterIdempotency() {
var order = GrpcServerInterceptorOrder.stable();
org.assertj.core.api.Assertions.assertThat(
order.indexOf(GrpcServerInterceptorStage.VALIDATION))
.isGreaterThan(
order.indexOf(GrpcServerInterceptorStage.IDEMPOTENCY));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerInterceptorOrderTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcServerInterceptorStage {
EXCEPTION_BOUNDARY, TRACE, AUTHENTICATION,
ACTOR_TENANT, AUTHORIZATION, ADMISSION,
DEADLINE_CANCELLATION, IDEMPOTENCY,
VALIDATION, SERVICE_ADAPTER
}
public record GrpcServerInterceptorOrder(
java.util.List<GrpcServerInterceptorStage> stages) {
public static GrpcServerInterceptorOrder stable() {
return new GrpcServerInterceptorOrder(
java.util.List.of(
GrpcServerInterceptorStage.EXCEPTION_BOUNDARY,
GrpcServerInterceptorStage.TRACE,
GrpcServerInterceptorStage.AUTHENTICATION,
GrpcServerInterceptorStage.ACTOR_TENANT,
GrpcServerInterceptorStage.AUTHORIZATION,
GrpcServerInterceptorStage.ADMISSION,
GrpcServerInterceptorStage.DEADLINE_CANCELLATION,
GrpcServerInterceptorStage.IDEMPOTENCY,
GrpcServerInterceptorStage.VALIDATION,
GrpcServerInterceptorStage.SERVICE_ADAPTER));
}
public int indexOf(GrpcServerInterceptorStage stage) {
return stages.indexOf(stage);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerInterceptorOrderTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorStage.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrder.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorChain.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerInterceptorOrderTest.java'
git commit -m "feat: fix grpc server interceptor order"
Task 16: Context Propagation과 Thread 경계
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextBinder.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextSnapshot.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextPropagationPolicy.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/context/GrpcContextBinderTest.java
Interfaces:
- Consumes: Task 7 request context와 Task 15 interceptor order.
- Produces: gRPC Context, SecurityContext, Reactor Context, executor 경계에서 Actor·Tenant·Deadline을 보존하는 binder.
Implementation requirements:
-
context snapshot은 immutable하며 credential 원문을 포함하지 않는다.
-
blocking executor와 virtual thread에 context를 복원하고 작업 종료 후 정리한다.
-
Reactor adapter가 있을 때 Reactor Context와 gRPC Context를 명시적으로 bridge한다.
-
context가 없는 비동기 작업은 fail-closed한다.
-
stream 수명 동안 actor·tenant·credential expiry를 보존한다.
-
Step 1: Write the failing test
class GrpcContextBinderTest {
@org.junit.jupiter.api.Test
void missingContextFailsClosed() {
var binder = new GrpcContextBinder();
org.assertj.core.api.Assertions.assertThatThrownBy(
binder::current)
.isInstanceOf(IllegalStateException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.context.GrpcContextBinderTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcContextSnapshot(
GrpcRequestContext requestContext,
java.time.Instant capturedAt) {}
public final class GrpcContextBinder {
private static final ThreadLocal<GrpcContextSnapshot> CURRENT =
new ThreadLocal<>();
public GrpcContextSnapshot current() {
var value = CURRENT.get();
if (value == null) {
throw new IllegalStateException(
"gRPC request context is required");
}
return value;
}
public AutoCloseable bind(GrpcContextSnapshot value) {
CURRENT.set(value);
return CURRENT::remove;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.context.GrpcContextBinderTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextBinder.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextSnapshot.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/context/GrpcContextPropagationPolicy.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/context/GrpcContextBinderTest.java'
git commit -m "feat: propagate grpc request context"
Task 17: Status와 Rich Error Mapper
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcStatusMapping.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcRichErrorDetail.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorMapper.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorExposurePolicy.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/error/GrpcErrorMapperTest.java
Interfaces:
- Consumes: Task 5 failure model과 Application exception hierarchy.
- Produces: 안정된 gRPC Status와 allowlisted google.rpc detail로 내부 오류를 변환하는 mapper.
Implementation requirements:
-
validation은
INVALID_ARGUMENT, state precondition은FAILED_PRECONDITION, concurrency abort는ABORTED로 매핑한다. -
retry hint는
RetryInfodetail로만 노출한다. -
unknown exception은
INTERNAL과 opaque execution ID로 변환한다. -
stack, SQL, query, host, token, PII를 detail에서 제거한다.
-
client는 message 문자열이 아니라 code·reason·typed detail로 분기한다.
-
Step 1: Write the failing test
class GrpcErrorMapperTest {
@org.junit.jupiter.api.Test
void unknownFailureIsOpaqueInternalError() {
var mapped = new GrpcErrorMapper(
GrpcErrorExposurePolicy.safe())
.map(new RuntimeException("jdbc:secret"));
org.assertj.core.api.Assertions.assertThat(mapped.code())
.isEqualTo("INTERNAL");
org.assertj.core.api.Assertions.assertThat(mapped.message())
.doesNotContain("jdbc");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.error.GrpcErrorMapperTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcStatusMapping(
String code,
String message,
java.util.List<GrpcRichErrorDetail> details) {}
public record GrpcRichErrorDetail(
String type,
java.util.Map<String, String> safeFields) {}
public record GrpcErrorExposurePolicy(
java.util.Set<String> allowedDetailTypes) {
public static GrpcErrorExposurePolicy safe() {
return new GrpcErrorExposurePolicy(java.util.Set.of(
"BadRequest", "PreconditionFailure", "RetryInfo",
"ResourceInfo", "ErrorInfo", "QuotaFailure",
"LocalizedMessage"));
}
}
public final class GrpcErrorMapper {
public GrpcErrorMapper(GrpcErrorExposurePolicy policy) {}
public GrpcStatusMapping map(Throwable error) {
return new GrpcStatusMapping(
"INTERNAL", "RPC execution failed",
java.util.List.of(new GrpcRichErrorDetail(
"ErrorInfo",
java.util.Map.of("reason", "INTERNAL_ERROR"))));
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.error.GrpcErrorMapperTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcStatusMapping.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcRichErrorDetail.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorMapper.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/error/GrpcErrorExposurePolicy.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/error/GrpcErrorMapperTest.java'
git commit -m "feat: add grpc status and rich error mapping"
Task 18: Netty Server Profile·Executor·Admission
Files:
- Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerProfile.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerTransport.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcExecutorProfile.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcAdmissionController.java - Test:
modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerProfileTest.java
Interfaces:
- Consumes: Task 15 interceptor chain과 Spring Boot gRPC server customization.
- Produces: Netty Stable server의 size, metadata, concurrency, executor, keepalive, drain 설정.
Implementation requirements:
-
Netty를 Stable 기본 transport로 사용한다.
-
blocking Use Case는 bounded executor에서 실행하며 event loop 직접 실행을 금지한다.
-
queue와 concurrent call limit을 넘으면
RESOURCE_EXHAUSTED로 거부한다. -
directExecutor와 unbounded queue를 production에서 금지한다. -
message·metadata·keepalive·connection age·shutdown 설정을 profile로 검증한다.
-
Step 1: Write the failing test
class GrpcServerProfileTest {
@org.junit.jupiter.api.Test
void productionRequiresBoundedExecutorAndLimits() {
var profile = GrpcServerProfile.stableNetty();
org.assertj.core.api.Assertions.assertThat(
profile.maxConcurrentCalls()).isPositive();
org.assertj.core.api.Assertions.assertThat(
profile.executor().bounded()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerProfileTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcServerTransport {
NETTY, NETTY_SHADED, SERVLET, IN_PROCESS
}
public record GrpcExecutorProfile(
String name,
int maxConcurrency,
int queueCapacity,
boolean bounded) {}
public record GrpcServerProfile(
GrpcServerTransport transport,
int maxInboundMessageBytes,
int maxInboundMetadataBytes,
int maxConcurrentCalls,
GrpcExecutorProfile executor) {
public static GrpcServerProfile stableNetty() {
return new GrpcServerProfile(
GrpcServerTransport.NETTY,
4 * 1024 * 1024,
8 * 1024,
512,
new GrpcExecutorProfile(
"blocking-bounded", 256, 256, true));
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcServerProfileTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerProfile.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcServerTransport.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcExecutorProfile.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcAdmissionController.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcServerProfileTest.java'
git commit -m "feat: configure stable grpc netty server"
Task 19: Netty Shaded Transport 호환 Profile
Files:
- Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariant.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelector.java - Create:
modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyParityContract.java - Test:
modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelectorTest.java
Interfaces:
- Consumes: Task 18 Stable Netty profile.
- Produces:
grpc-netty와grpc-netty-shaded를 상호 배타적으로 선택하고 기능 동등성을 검증하는 profile.
Implementation requirements:
-
한 runtime에 unshaded와 shaded transport가 동시에 활성화되지 않게 한다.
-
TLS, metadata limit, message limit, health, reflection, graceful shutdown parity를 검증한다.
-
shaded profile은 dependency conflict 회피 목적이며 기능을 추가하지 않는다.
-
variant 선택은 build/runtime profile에서 명시한다.
-
실제 Netty integration suite를 두 variant에 실행한다.
-
Step 1: Write the failing test
class GrpcNettyVariantSelectorTest {
@org.junit.jupiter.api.Test
void twoNettyVariantsCannotBeEnabledTogether() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcNettyVariantSelector(
java.util.Set.of(
GrpcNettyVariant.NETTY,
GrpcNettyVariant.NETTY_SHADED)))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcNettyVariantSelectorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcNettyVariant {
NETTY, NETTY_SHADED
}
public record GrpcNettyVariantSelector(
java.util.Set<GrpcNettyVariant> enabled) {
public GrpcNettyVariantSelector {
if (enabled == null || enabled.size() != 1) {
throw new IllegalArgumentException(
"exactly one Netty variant is required");
}
}
}
public record GrpcNettyParityContract(
boolean tls,
boolean metadataLimit,
boolean gracefulShutdown) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-server:test --tests 'io.backend.skeleton.grpc.server.GrpcNettyVariantSelectorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariant.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelector.java' 'modules/grpc/grpc-server/src/main/java/io/backend/skeleton/grpc/server/GrpcNettyParityContract.java' 'modules/grpc/grpc-server/src/test/java/io/backend/skeleton/grpc/server/GrpcNettyVariantSelectorTest.java'
git commit -m "build: add shaded netty compatibility profile"
Task 20: TLS·mTLS·CallCredentials와 Rotation
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcTlsProfile.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcAuthenticationProfile.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialGeneration.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialRotationManager.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/security/GrpcTlsProfileTest.java
Interfaces:
- Consumes: Spring Boot SSL Bundle, Security token source와 Task 24 예정 Named Channel profile.
- Produces: TLS/mTLS와 per-call credential을 분리하고 generation 기반으로 교체하는 security contract.
Implementation requirements:
-
dev·stage·prod에서 TLS를 필수로 한다.
-
mTLS는 service identity가 필요한 profile에서 사용한다.
-
Bearer/JWT/service token은
CallCredentials또는 승인된 credential adapter가 생성한다. -
trust-all과 hostname verification 비활성화를 production에서 startup failure로 처리한다.
-
credential·certificate 교체 시 새 generation을 만들고 기존 call/stream을 drain한다.
-
Step 1: Write the failing test
class GrpcTlsProfileTest {
@org.junit.jupiter.api.Test
void productionRejectsTrustAll() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> GrpcTlsProfile.production(
"internal-ca", false, true))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.security.GrpcTlsProfileTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcTlsProfile(
String sslBundle,
boolean hostnameVerification,
boolean trustAll,
boolean mutualTls) {
public static GrpcTlsProfile production(
String bundle,
boolean hostnameVerification,
boolean trustAll) {
if (!hostnameVerification || trustAll) {
throw new IllegalArgumentException(
"secure TLS verification is required");
}
return new GrpcTlsProfile(
bundle, true, false, false);
}
}
public record GrpcCredentialGeneration(
long generation,
java.time.Instant createdAt) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.security.GrpcTlsProfileTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcTlsProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcAuthenticationProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialGeneration.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/security/GrpcCredentialRotationManager.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/security/GrpcTlsProfileTest.java'
git commit -m "feat: add grpc tls and credential rotation"
Task 21: 표준 Health Service와 Service 상태
Files:
- Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthState.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistry.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthPolicy.java - Test:
modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistryTest.java
Interfaces:
- Consumes: Server service catalog와 dependency health signals.
- Produces: global·service별 health, readiness, draining과 Watch를 제공하는 표준 health registry.
Implementation requirements:
-
상태는
UNKNOWN,SERVING,NOT_SERVING,SERVICE_UNKNOWN,DRAINING으로 표현한다. -
startup 중에는 요청을 받기 전에 readiness를 false로 유지한다.
-
drain 시작 시 service health를 먼저
DRAINING/NOT_SERVING으로 전환한다. -
dependency health가 business correctness에 필수일 때만 service health에 반영한다.
-
health endpoint의 network·credential 접근 정책을 business RPC와 분리한다.
-
Step 1: Write the failing test
class GrpcServiceHealthRegistryTest {
@org.junit.jupiter.api.Test
void drainingServiceIsNotReadyForNewCalls() {
var registry = new GrpcServiceHealthRegistry();
registry.draining("DocumentService");
org.assertj.core.api.Assertions.assertThat(
registry.state("DocumentService"))
.isEqualTo(GrpcHealthState.DRAINING);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcServiceHealthRegistryTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcHealthState {
UNKNOWN, SERVING, NOT_SERVING,
SERVICE_UNKNOWN, DRAINING
}
public final class GrpcServiceHealthRegistry {
private final java.util.concurrent.ConcurrentMap<String,
GrpcHealthState> states =
new java.util.concurrent.ConcurrentHashMap<>();
public void draining(String service) {
states.put(service, GrpcHealthState.DRAINING);
}
public GrpcHealthState state(String service) {
return states.getOrDefault(
service, GrpcHealthState.UNKNOWN);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcServiceHealthRegistryTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthState.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistry.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcHealthPolicy.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcServiceHealthRegistryTest.java'
git commit -m "feat: add grpc health registry"
Task 22: Reflection 환경·권한 정책
Files:
- Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionMode.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicy.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionAccessDecision.java - Test:
modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicyTest.java
Interfaces:
- Consumes: Schema descriptor artifact와 environment/security profile.
- Produces: Local/Test 허용, Dev 관리자 인증, Production 기본 비활성인 reflection policy.
Implementation requirements:
-
Reflection mode는
ENABLED,ADMIN_ONLY,DISABLED다. -
production default는
DISABLED다. -
Admin-only reflection은 별도 network/role predicate를 요구한다.
-
reflection 공개 여부와 실제 RPC authorization을 동일시하지 않는다.
-
schema descriptor version을 admin snapshot에 노출하되 secret은 제외한다.
-
Step 1: Write the failing test
class GrpcReflectionPolicyTest {
@org.junit.jupiter.api.Test
void productionDefaultsToDisabled() {
org.assertj.core.api.Assertions.assertThat(
GrpcReflectionPolicy.production().mode())
.isEqualTo(GrpcReflectionMode.DISABLED);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcReflectionPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcReflectionMode {
ENABLED, ADMIN_ONLY, DISABLED
}
public record GrpcReflectionPolicy(
GrpcReflectionMode mode,
java.util.Set<String> requiredRoles) {
public static GrpcReflectionPolicy production() {
return new GrpcReflectionPolicy(
GrpcReflectionMode.DISABLED,
java.util.Set.of());
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcReflectionPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionMode.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicy.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcReflectionAccessDecision.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcReflectionPolicyTest.java'
git commit -m "feat: govern grpc reflection access"
Task 23: Graceful Shutdown과 Stream Drain
Files:
- Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPhase.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPolicy.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinator.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainResult.java - Test:
modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinatorTest.java
Interfaces:
- Consumes: Task 21 health registry, server call registry와 streaming session registry.
- Produces: readiness 차단부터 unary·stream drain과 force cancel까지의 deterministic shutdown.
Implementation requirements:
-
순서는 readiness false → health draining → new admission reject → unary drain → stream signal → force cancel이다.
-
drain과 force timeout을 분리한다.
-
stream에는 마지막 sequence·resume cursor 또는 full resync 필요 상태를 전달한다.
-
drain 중 새 retry·new stream을 시작하지 않는다.
-
drain 결과에 completed·cancelled·timed-out call 수를 보존한다.
-
Step 1: Write the failing test
class GrpcDrainCoordinatorTest {
@org.junit.jupiter.api.Test
void healthChangesBeforeNewAdmissionStops() {
var coordinator = new GrpcDrainCoordinator(
GrpcDrainPolicy.standard());
var phases = coordinator.plan();
org.assertj.core.api.Assertions.assertThat(phases)
.containsSubsequence(
GrpcDrainPhase.READINESS_OFF,
GrpcDrainPhase.HEALTH_DRAINING,
GrpcDrainPhase.ADMISSION_CLOSED);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcDrainCoordinatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcDrainPhase {
READINESS_OFF,
HEALTH_DRAINING,
ADMISSION_CLOSED,
UNARY_DRAIN,
STREAM_SIGNAL,
FORCE_CANCEL,
COMPLETE
}
public record GrpcDrainPolicy(
java.time.Duration drainTimeout,
java.time.Duration forceTimeout) {
public static GrpcDrainPolicy standard() {
return new GrpcDrainPolicy(
java.time.Duration.ofSeconds(30),
java.time.Duration.ofSeconds(5));
}
}
public final class GrpcDrainCoordinator {
public GrpcDrainCoordinator(GrpcDrainPolicy policy) {}
public java.util.List<GrpcDrainPhase> plan() {
return java.util.List.of(
GrpcDrainPhase.READINESS_OFF,
GrpcDrainPhase.HEALTH_DRAINING,
GrpcDrainPhase.ADMISSION_CLOSED,
GrpcDrainPhase.UNARY_DRAIN,
GrpcDrainPhase.STREAM_SIGNAL,
GrpcDrainPhase.FORCE_CANCEL,
GrpcDrainPhase.COMPLETE);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcDrainCoordinatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPhase.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainPolicy.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinator.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcDrainResult.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcDrainCoordinatorTest.java'
git commit -m "feat: implement grpc graceful drain"
Task 24: Named Channel Profile
Files:
- Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfile.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcLoadBalancingPolicy.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcRetryOwner.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelProfileValidator.java - Test:
modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfileTest.java
Interfaces:
- Consumes: Task 3 method policies, Task 20 TLS profiles와 deployment configuration.
- Produces: logical channel name별 target, transport, LB, TLS, deadline, retry owner, size limit 계약.
Implementation requirements:
-
target은 resolver scheme을 포함한 URI 형태로 검증한다.
-
load balancing은 Stable에서
PICK_FIRST,ROUND_ROBIN만 허용한다. -
retry owner는
GRPC_PLATFORM,SERVICE_MESH,APPLICATION,NONE중 하나다. -
TLS, metadata/message limit, wait-for-ready, observability 설정을 profile에 고정한다.
-
동일 service라도 long stream과 short unary의 SLO가 다르면 별도 channel profile을 사용한다.
-
Step 1: Write the failing test
class GrpcNamedChannelProfileTest {
@org.junit.jupiter.api.Test
void meshOwnedRetryCannotAlsoUseGrpcRetry() {
var profile = GrpcNamedChannelProfile.mesh(
new GrpcChannelProfileName("document"));
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcChannelProfileValidator()
.validate(profile, true))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcNamedChannelProfileTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcLoadBalancingPolicy {
PICK_FIRST, ROUND_ROBIN
}
public enum GrpcRetryOwner {
GRPC_PLATFORM, SERVICE_MESH, APPLICATION, NONE
}
public record GrpcNamedChannelProfile(
GrpcChannelProfileName name,
String target,
GrpcLoadBalancingPolicy loadBalancing,
GrpcRetryOwner retryOwner,
GrpcTlsProfile tls,
int maxInboundMessageBytes,
int maxMetadataBytes) {
public static GrpcNamedChannelProfile mesh(
GrpcChannelProfileName name) {
return new GrpcNamedChannelProfile(
name, "dns:///service:9090",
GrpcLoadBalancingPolicy.PICK_FIRST,
GrpcRetryOwner.SERVICE_MESH,
GrpcTlsProfile.production(
"internal-ca", true, false),
4 * 1024 * 1024, 8 * 1024);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcNamedChannelProfileTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfile.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcLoadBalancingPolicy.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcRetryOwner.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelProfileValidator.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcNamedChannelProfileTest.java'
git commit -m "feat: define grpc named channel profiles"
Task 25: Channel Runtime Generation과 Drain
Files:
- Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelGeneration.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntime.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistry.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelDrainPolicy.java - Test:
modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistryTest.java
Interfaces:
- Consumes: Task 24 Named Channel Profile과 credential generation.
- Produces: Channel·Stub을 재사용하고 설정·인증서 교체 시 새 generation으로 무중단 전환하는 runtime registry.
Implementation requirements:
-
요청마다 channel을 생성하지 않는다.
-
profile generation 변경 시 새 runtime을 준비한 뒤 atomic pointer를 교체한다.
-
기존 runtime은 in-flight unary와 stream을 drain한다.
-
drain timeout 후 남은 call을 cancel하고 metric을 기록한다.
-
runtime registry는 credential·raw builder를 application에 반환하지 않는다.
-
Step 1: Write the failing test
class GrpcChannelRuntimeRegistryTest {
@org.junit.jupiter.api.Test
void rotationCreatesNewGenerationBeforeOldDrain() {
var registry = new GrpcChannelRuntimeRegistry();
var first = registry.install("document");
var second = registry.rotate("document");
org.assertj.core.api.Assertions.assertThat(
second.generation()).isGreaterThan(first.generation());
org.assertj.core.api.Assertions.assertThat(
registry.current("document")).isEqualTo(second);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcChannelRuntimeRegistryTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcChannelGeneration(long value) {}
public record GrpcChannelRuntime(
String profile,
GrpcChannelGeneration generation,
boolean acceptingCalls) {}
public final class GrpcChannelRuntimeRegistry {
private final java.util.concurrent.ConcurrentMap<String,
GrpcChannelRuntime> current =
new java.util.concurrent.ConcurrentHashMap<>();
public GrpcChannelRuntime install(String profile) {
var runtime = new GrpcChannelRuntime(
profile, new GrpcChannelGeneration(1), true);
current.put(profile, runtime);
return runtime;
}
public GrpcChannelRuntime rotate(String profile) {
var old = current(profile);
var next = new GrpcChannelRuntime(
profile,
new GrpcChannelGeneration(
old.generation().value() + 1),
true);
current.put(profile, next);
return next;
}
public GrpcChannelRuntime current(String profile) {
return current.get(profile);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcChannelRuntimeRegistryTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelGeneration.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntime.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistry.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcChannelDrainPolicy.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcChannelRuntimeRegistryTest.java'
git commit -m "feat: manage grpc channel generations"
Task 26: Typed Stub Factory와 Method Policy 적용
Files:
- Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactory.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubDescriptor.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubPolicyApplier.java - Test:
modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactoryTest.java
Interfaces:
- Consumes: Task 24 channel profile, Task 25 runtime registry, generated stub classes.
- Produces: registered generated stub만 생성하고 method policy·deadline·credentials를 적용하는 typed factory.
Implementation requirements:
-
stub factory는 allowlisted generated service type만 생성한다.
-
stub마다 Named Channel Profile과 method policy catalog를 연결한다.
-
raw Channel·ManagedChannel과 builder를 application에 반환하지 않는다.
-
blocking, future, async stub 유형을 descriptor로 구분한다.
-
method 호출 직전에 effective deadline과 call credentials를 적용한다.
-
Step 1: Write the failing test
class GrpcTypedStubFactoryTest {
@org.junit.jupiter.api.Test
void unregisteredStubTypeIsRejected() {
var factory = GrpcTypedStubFactory.empty();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> factory.create(Object.class, "document"))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcTypedStubFactoryTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcStubDescriptor(
Class<?> stubType,
String profile,
String style) {}
public final class GrpcTypedStubFactory {
private final java.util.Map<Class<?>, GrpcStubDescriptor> types;
private GrpcTypedStubFactory(
java.util.Map<Class<?>, GrpcStubDescriptor> types) {
this.types = types;
}
public static GrpcTypedStubFactory empty() {
return new GrpcTypedStubFactory(java.util.Map.of());
}
public <T> T create(Class<T> type, String profile) {
if (!types.containsKey(type)) {
throw new IllegalArgumentException(
"unregistered generated stub");
}
throw new UnsupportedOperationException(
"transport adapter creates the generated stub");
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcTypedStubFactoryTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactory.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubDescriptor.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcStubPolicyApplier.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcTypedStubFactoryTest.java'
git commit -m "feat: add grpc typed stub factory"
Task 27: Client Metadata와 CallCredentials
Files:
- Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicy.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcCallCredentialProvider.java - Create:
modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientCallContext.java - Test:
modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicyTest.java
Interfaces:
- Consumes: Task 7 metadata keys, Task 20 credential profiles와 Task 26 stub factory.
- Produces: trace·correlation·idempotency metadata와 per-call credential을 안전하게 materialize하는 client policy.
Implementation requirements:
-
Authorization은 application-provided generic header가 아니라 credential provider가 생성한다.
-
tenant·actor context는 allowlisted signed/validated representation만 전달한다.
-
cross-trust-boundary channel에서 baggage·tenant metadata allowlist를 별도 적용한다.
-
metadata budget을 call 시작 전에 검증한다.
-
token, cookie, password와 raw PII를 log·metric에 기록하지 않는다.
-
Step 1: Write the failing test
class GrpcClientMetadataPolicyTest {
@org.junit.jupiter.api.Test
void applicationCannotOverrideAuthorizationMetadata() {
var policy = GrpcClientMetadataPolicy.standard();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> policy.validateUserKey("authorization"))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcClientMetadataPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcClientMetadataPolicy(
java.util.Set<String> reservedKeys,
GrpcMetadataBudget budget) {
public static GrpcClientMetadataPolicy standard() {
return new GrpcClientMetadataPolicy(
java.util.Set.of(
"authorization", "grpc-timeout",
"traceparent", "tracestate"),
new GrpcMetadataBudget(8192, 4096));
}
public void validateUserKey(String key) {
if (reservedKeys.contains(key.toLowerCase())) {
throw new IllegalArgumentException(
"reserved gRPC metadata key");
}
}
}
public interface GrpcCallCredentialProvider {
Object credentials(GrpcClientCallContext context);
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-client:test --tests 'io.backend.skeleton.grpc.client.GrpcClientMetadataPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicy.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcCallCredentialProvider.java' 'modules/grpc/grpc-client/src/main/java/io/backend/skeleton/grpc/client/GrpcClientCallContext.java' 'modules/grpc/grpc-client/src/test/java/io/backend/skeleton/grpc/client/GrpcClientMetadataPolicyTest.java'
git commit -m "feat: secure grpc client metadata"
Task 28: Deadline 전파와 Dependency Budget
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculator.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDependencyBudget.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlinePolicyValidator.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculatorTest.java
Interfaces:
- Consumes: Task 6 deadline primitive, Task 3 method policy와 current gRPC Context.
- Produces: parent remaining deadline과 method profile에서 downstream call budget을 계산하는 policy.
Implementation requirements:
-
effective deadline은 parent remaining과 method default 중 더 짧다.
-
safety margin과 response/trailer reserve를 차감한다.
-
DB·HTTP·downstream gRPC timeout이 inbound deadline을 초과하면 startup 검증에서 실패한다.
-
minimum attempt budget보다 짧으면 call을 시작하지 않는다.
-
deadline이 없는 Stable Unary call을 거부한다.
-
Step 1: Write the failing test
class GrpcDeadlineCalculatorTest {
@org.junit.jupiter.api.Test
void childDeadlineReservesResponseBudget() {
var calculator = new GrpcDeadlineCalculator(
java.time.Duration.ofMillis(100));
var child = calculator.calculate(
java.time.Duration.ofSeconds(2),
java.time.Duration.ofSeconds(5));
org.assertj.core.api.Assertions.assertThat(child)
.isLessThanOrEqualTo(
java.time.Duration.ofMillis(1900));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineCalculatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public final class GrpcDeadlineCalculator {
private final java.time.Duration reserve;
public GrpcDeadlineCalculator(
java.time.Duration reserve) {
this.reserve = reserve;
}
public java.time.Duration calculate(
java.time.Duration parentRemaining,
java.time.Duration methodDefault) {
var minimum = parentRemaining.compareTo(methodDefault) < 0
? parentRemaining : methodDefault;
var result = minimum.minus(reserve);
if (result.isZero() || result.isNegative()) {
throw new GrpcDeadlineExceededException(
"insufficient downstream budget");
}
return result;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcDeadlineCalculatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDependencyBudget.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcDeadlinePolicyValidator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcDeadlineCalculatorTest.java'
git commit -m "feat: propagate grpc deadline budgets"
Task 29: Cancellation 전파와 Side-effect 차단
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinator.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellableOperation.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationReason.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinatorTest.java
Interfaces:
- Consumes: Task 6 cancellation token과 Application dependency cancellation adapters.
- Produces: client cancel/deadline을 Future·Publisher·HTTP·DB 작업에 전파하고 새 side effect 시작을 차단하는 coordinator.
Implementation requirements:
-
cancel은 idempotent하며 첫 reason을 보존한다.
-
Application Use Case는 cancellation token을 인자로 받거나 context에서 조회한다.
-
cancel 이후 신규 외부 side effect를 시작하지 않는다.
-
commit 경계 이후 cancel은 business evidence를 자동 ABORT로 바꾸지 않는다.
-
stream source와 writer를 모두 cancel하고 queue를 정리한다.
-
Step 1: Write the failing test
class GrpcCancellationCoordinatorTest {
@org.junit.jupiter.api.Test
void cancellationInvokesRegisteredOperationsOnce() {
var calls = new java.util.concurrent.atomic.AtomicInteger();
var coordinator = new GrpcCancellationCoordinator();
coordinator.register(calls::incrementAndGet);
coordinator.cancel(GrpcCancellationReason.CLIENT_CANCELLED);
coordinator.cancel(GrpcCancellationReason.DEADLINE);
org.assertj.core.api.Assertions.assertThat(calls).hasValue(1);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcCancellationCoordinatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcCancellationReason {
CLIENT_CANCELLED, DEADLINE, SERVER_DRAIN, POLICY
}
@FunctionalInterface
public interface GrpcCancellableOperation {
void cancel();
}
public final class GrpcCancellationCoordinator {
private final java.util.List<GrpcCancellableOperation> operations =
new java.util.concurrent.CopyOnWriteArrayList<>();
private final java.util.concurrent.atomic.AtomicBoolean cancelled =
new java.util.concurrent.atomic.AtomicBoolean();
public void register(GrpcCancellableOperation operation) {
if (cancelled.get()) {
operation.cancel();
} else {
operations.add(operation);
}
}
public void cancel(GrpcCancellationReason reason) {
if (cancelled.compareAndSet(false, true)) {
operations.forEach(GrpcCancellableOperation::cancel);
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.deadline.GrpcCancellationCoordinatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellableOperation.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/deadline/GrpcCancellationReason.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/deadline/GrpcCancellationCoordinatorTest.java'
git commit -m "feat: propagate grpc cancellation"
Task 30: Service Config와 Retry Owner 검증
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcServiceConfigPolicy.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcMethodRetryConfig.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidator.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidatorTest.java
Interfaces:
- Consumes: Task 24 channel retry owner와 Task 3 method policy.
- Produces: method별 timeout·wait-for-ready·retry를 하나의 owner만 관리하게 하는 Service Config contract.
Implementation requirements:
-
APPLICATION, GRPC_PLATFORM, SERVICE_MESH, NONE 중 explicit retry owner는 하나다.
-
Service Config의 method name이 policy catalog와 일치해야 한다.
-
mesh owner일 때 gRPC explicit retry와 hedging을 비활성화한다.
-
retry policy가 없는 것과 channel retry 자체를 disable한 것을 구분한다.
-
transparent retry 존재 여부를 운영 snapshot에 기록한다.
-
Step 1: Write the failing test
class GrpcRetryOwnershipValidatorTest {
@org.junit.jupiter.api.Test
void twoExplicitRetryOwnersAreRejected() {
var validator = new GrpcRetryOwnershipValidator();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> validator.validate(
java.util.Set.of(
GrpcRetryOwner.GRPC_PLATFORM,
GrpcRetryOwner.SERVICE_MESH)))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryOwnershipValidatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcMethodRetryConfig(
GrpcMethodName method,
int maxAttempts,
java.time.Duration initialBackoff,
java.util.Set<String> retryableStatusCodes) {}
public final class GrpcRetryOwnershipValidator {
public void validate(
java.util.Set<GrpcRetryOwner> explicitOwners) {
var count = explicitOwners.stream()
.filter(owner -> owner != GrpcRetryOwner.NONE)
.count();
if (count > 1) {
throw new IllegalArgumentException(
"one explicit retry owner is required");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryOwnershipValidatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcServiceConfigPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcMethodRetryConfig.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryOwnershipValidatorTest.java'
git commit -m "feat: validate grpc retry ownership"
Task 31: Retry Eligibility와 Budget
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryDecision.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryBudget.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibility.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryCoordinator.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibilityTest.java
Interfaces:
- Consumes: Task 3 idempotency profile, Task 4 evidence, Task 29 retry ownership과 deadline budget.
- Produces: status·idempotency·evidence·deadline·partial delivery를 함께 판정하는 retry coordinator.
Implementation requirements:
-
NON_IDEMPOTENT에 explicit retry를 허용하지 않는다. -
IDEMPOTENCY_KEY_REQUIRED는 key와 ledger capability가 모두 있을 때만 retry 후보가 된다. -
DEADLINE_EXCEEDEDmutation은 retry 대신 Completion Unknown을 반환한다. -
partial stream delivery 이후 whole-call retry를 금지한다.
-
backoff, jitter, maximum attempts, maximum elapsed, retry budget을 적용한다.
-
Step 1: Write the failing test
class GrpcRetryEligibilityTest {
@org.junit.jupiter.api.Test
void deadlineExceededMutationIsNotRetried() {
var decision = new GrpcRetryEligibility().decide(
RpcIdempotencyProfile.IDEMPOTENCY_KEY_REQUIRED,
"DEADLINE_EXCEEDED",
GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT,
false,
java.time.Duration.ofSeconds(1));
org.assertj.core.api.Assertions.assertThat(
decision.retry()).isFalse();
org.assertj.core.api.Assertions.assertThat(
decision.completionUnknown()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryEligibilityTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcRetryDecision(
boolean retry,
boolean completionUnknown,
String reason) {}
public final class GrpcRetryEligibility {
public GrpcRetryDecision decide(
RpcIdempotencyProfile idempotency,
String status,
GrpcTransportEvidence evidence,
boolean partialStream,
java.time.Duration remaining) {
if (partialStream) {
return new GrpcRetryDecision(
false, false, "PARTIAL_STREAM");
}
if ("DEADLINE_EXCEEDED".equals(status)
&& idempotency !=
RpcIdempotencyProfile.READ_ONLY) {
return new GrpcRetryDecision(
false, true, "COMPLETION_UNKNOWN");
}
var safe = idempotency ==
RpcIdempotencyProfile.READ_ONLY
&& remaining.compareTo(
java.time.Duration.ofMillis(100)) > 0;
return new GrpcRetryDecision(
safe, false, safe ? "READ_RETRY" : "DENY");
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcRetryEligibilityTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryDecision.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryBudget.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibility.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcRetryCoordinator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcRetryEligibilityTest.java'
git commit -m "feat: implement grpc retry eligibility"
Task 32: JPA Operation Ledger Schema와 Repository
Files:
- Create:
modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerEntity.java - Create:
modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerState.java - Create:
modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepository.java - Create:
modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationIdentity.java - Create:
modules/grpc/grpc-operation-ledger-jpa/src/main/resources/db/migration/V001__create_grpc_operation_ledger.sql - Test:
modules/grpc/grpc-operation-ledger-jpa/src/test/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepositoryTest.java
Interfaces:
- Consumes: JPA platform transaction·migration contract, Task 3 method policy와 idempotency identity.
- Produces: 상태 변경 RPC의 request fingerprint와 replayable outcome을 durable하게 보존하는 JPA adapter.
Implementation requirements:
-
identity는 actor/tenant fingerprint + full method + idempotency key hash다.
-
DB unique constraint로 동일 identity의 단일 row를 보장한다.
-
state는
IN_PROGRESS,COMMITTED,FAILED_TERMINAL이다. -
request fingerprint mismatch를 저장·감지한다.
-
response payload는 size limit 안에서 저장하거나 object reference를 사용한다.
-
Step 1: Write the failing test
class GrpcOperationLedgerRepositoryTest {
@org.junit.jupiter.api.Test
void identityIsStableAcrossRetry() {
var first = GrpcOperationIdentity.of(
"actor-hash", "tenant-hash",
"x.y.Command/Create", "key-hash");
var second = GrpcOperationIdentity.of(
"actor-hash", "tenant-hash",
"x.y.Command/Create", "key-hash");
org.assertj.core.api.Assertions.assertThat(first)
.isEqualTo(second);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-operation-ledger-jpa:test --tests 'io.backend.skeleton.grpc.ledger.GrpcOperationLedgerRepositoryTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcOperationLedgerState {
IN_PROGRESS, COMMITTED, FAILED_TERMINAL
}
public record GrpcOperationIdentity(
String actorFingerprint,
String tenantFingerprint,
String method,
String idempotencyKeyHash) {
public static GrpcOperationIdentity of(
String actor, String tenant,
String method, String key) {
return new GrpcOperationIdentity(
actor, tenant, method, key);
}
}
public interface GrpcOperationLedgerRepository {
java.util.Optional<GrpcOperationLedgerEntity> find(
GrpcOperationIdentity identity);
void save(GrpcOperationLedgerEntity entity);
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-operation-ledger-jpa:test --tests 'io.backend.skeleton.grpc.ledger.GrpcOperationLedgerRepositoryTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerEntity.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerState.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepository.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/java/io/backend/skeleton/grpc/ledger/GrpcOperationIdentity.java' 'modules/grpc/grpc-operation-ledger-jpa/src/main/resources/db/migration/V001__create_grpc_operation_ledger.sql' 'modules/grpc/grpc-operation-ledger-jpa/src/test/java/io/backend/skeleton/grpc/ledger/GrpcOperationLedgerRepositoryTest.java'
git commit -m "feat: add grpc operation ledger persistence"
Task 33: Idempotency Interceptor와 Outcome Replay
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptor.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcRequestFingerprint.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyDecision.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOutcomeReplay.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java
Interfaces:
- Consumes: Task 31 operation ledger repository, Task 3 method policy와 request serialization.
- Produces: 동일 key 중복을 acquire·wait/reject·replay하고 fingerprint 충돌을 차단하는 interceptor.
Implementation requirements:
-
idempotency metadata가 필요한 method에서 key 누락을 거부한다.
-
같은 key와 같은 fingerprint의 COMMITTED row는 저장된 outcome을 replay한다.
-
같은 key와 다른 fingerprint는
FAILED_PRECONDITION이다. -
IN_PROGRESS 중복의 wait/poll/reject 정책을 method profile에 둔다.
-
업무 mutation과 ledger COMMITTED를 가능하면 같은 transaction에 기록한다.
-
Step 1: Write the failing test
class GrpcIdempotencyInterceptorTest {
@org.junit.jupiter.api.Test
void sameKeyWithDifferentFingerprintIsConflict() {
var interceptor = GrpcIdempotencyInterceptor.inMemory();
interceptor.acquire("key", new GrpcRequestFingerprint("a"));
var decision = interceptor.acquire(
"key", new GrpcRequestFingerprint("b"));
org.assertj.core.api.Assertions.assertThat(decision)
.isEqualTo(GrpcIdempotencyDecision.CONFLICT);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcIdempotencyInterceptorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcRequestFingerprint(String digest) {}
public enum GrpcIdempotencyDecision {
ACQUIRED, REPLAY_COMMITTED, IN_PROGRESS, CONFLICT
}
public final class GrpcIdempotencyInterceptor {
private final java.util.Map<String, GrpcRequestFingerprint> values =
new java.util.concurrent.ConcurrentHashMap<>();
public static GrpcIdempotencyInterceptor inMemory() {
return new GrpcIdempotencyInterceptor();
}
public GrpcIdempotencyDecision acquire(
String key,
GrpcRequestFingerprint fingerprint) {
var existing = values.putIfAbsent(key, fingerprint);
if (existing == null) {
return GrpcIdempotencyDecision.ACQUIRED;
}
return existing.equals(fingerprint)
? GrpcIdempotencyDecision.IN_PROGRESS
: GrpcIdempotencyDecision.CONFLICT;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcIdempotencyInterceptorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptor.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcRequestFingerprint.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyDecision.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOutcomeReplay.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java'
git commit -m "feat: enforce grpc idempotency ledger"
Task 34: Completion Unknown Query와 Reconciliation
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatus.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatusQuery.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconciler.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionResolution.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconcilerTest.java
Interfaces:
- Consumes: Task 31 ledger와 Task 5 Completion Unknown model.
- Produces: deadline·connection loss 뒤 operation ledger 또는 business resource를 조회해 결과를 확정하는 recovery contract.
Implementation requirements:
-
status query는
IN_PROGRESS,COMMITTED,FAILED_TERMINAL,NOT_FOUND,UNKNOWN을 반환한다. -
mutation body를 자동 재실행하기 전에 ledger를 우선 조회한다.
-
COMMITTED outcome이 있으면 동일 response 또는 stable result code를 반환한다.
-
결과를 확정할 수 없으면
UNKNOWN을 유지하고 reconciliation job으로 넘긴다. -
status query 자체는 read-only deadline·retry profile을 사용한다.
-
Step 1: Write the failing test
class GrpcCompletionReconcilerTest {
@org.junit.jupiter.api.Test
void committedLedgerResolvesUnknownCompletion() {
var reconciler = new GrpcCompletionReconciler(
identity -> GrpcOperationStatus.COMMITTED);
org.assertj.core.api.Assertions.assertThat(
reconciler.resolve(null))
.isEqualTo(GrpcCompletionResolution.COMMITTED);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcCompletionReconcilerTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcOperationStatus {
IN_PROGRESS, COMMITTED, FAILED_TERMINAL,
NOT_FOUND, UNKNOWN
}
public enum GrpcCompletionResolution {
COMMITTED, ABORTED, STILL_IN_PROGRESS, UNKNOWN
}
@FunctionalInterface
public interface GrpcOperationStatusQuery {
GrpcOperationStatus status(GrpcOperationIdentity identity);
}
public final class GrpcCompletionReconciler {
private final GrpcOperationStatusQuery query;
public GrpcCompletionReconciler(
GrpcOperationStatusQuery query) {
this.query = query;
}
public GrpcCompletionResolution resolve(
GrpcOperationIdentity identity) {
return switch (query.status(identity)) {
case COMMITTED -> GrpcCompletionResolution.COMMITTED;
case FAILED_TERMINAL -> GrpcCompletionResolution.ABORTED;
case IN_PROGRESS -> GrpcCompletionResolution.STILL_IN_PROGRESS;
default -> GrpcCompletionResolution.UNKNOWN;
};
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.idempotency.GrpcCompletionReconcilerTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatus.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcOperationStatusQuery.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconciler.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionResolution.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/idempotency/GrpcCompletionReconcilerTest.java'
git commit -m "feat: reconcile grpc completion unknown"
Task 35: DNS Resolver와 Stable Load Balancing
Files:
- Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverProfile.java - Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverType.java - Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcStableLoadBalancer.java - Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidator.java - Test:
modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidatorTest.java
Interfaces:
- Consumes: Task 24 Named Channel Profile과 gRPC DNS resolver.
- Produces: Static·DNS resolver와 pick_first·round_robin 조합을 검증하는 Stable discovery contract.
Implementation requirements:
-
Stable resolver는
STATIC,DNS다. -
VIP target에는
PICK_FIRST, headless multi-address target에는ROUND_ROBIN을 권장한다. -
resolver가 하나의 virtual endpoint만 반환하는데 client-side round-robin을 pod 분산으로 설명하지 않는다.
-
DNS change와 pooled connection 수명을 integration test한다.
-
custom resolver와 xDS는 Advanced로 차단한다.
-
Step 1: Write the failing test
class GrpcDiscoveryPolicyValidatorTest {
@org.junit.jupiter.api.Test
void xdsIsNotStableResolver() {
var validator = new GrpcDiscoveryPolicyValidator();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> validator.validateStable(
new GrpcResolverProfile(
GrpcResolverType.XDS,
GrpcLoadBalancingPolicy.ROUND_ROBIN)))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcDiscoveryPolicyValidatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcResolverType {
STATIC, DNS, UNIX, CUSTOM, XDS
}
public record GrpcResolverProfile(
GrpcResolverType type,
GrpcLoadBalancingPolicy loadBalancing) {}
public final class GrpcDiscoveryPolicyValidator {
public void validateStable(GrpcResolverProfile profile) {
if (profile.type() != GrpcResolverType.STATIC
&& profile.type() != GrpcResolverType.DNS) {
throw new IllegalArgumentException(
"resolver is not Stable");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcDiscoveryPolicyValidatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverProfile.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcResolverType.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcStableLoadBalancer.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidator.java' 'modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcDiscoveryPolicyValidatorTest.java'
git commit -m "feat: add stable grpc discovery profiles"
Task 36: Kubernetes VIP·Headless·Mesh Profile
Files:
- Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfile.java - Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesRoutingMode.java - Create:
modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileValidator.java - Test:
modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileTest.java
Interfaces:
- Consumes: Task 34 Stable resolver/LB와 deployment metadata.
- Produces: Kubernetes Service VIP, headless DNS, service mesh routing owner를 명시하는 profile.
Implementation requirements:
-
K8S_VIP은 service VIP + pick_first를 사용한다. -
K8S_HEADLESS는 multi-address DNS + round_robin을 사용한다. -
MESH는 mesh가 routing/retry owner이며 application explicit retry를 차단한다. -
장기 stream은 pod에 고정되므로 readiness·drain·reconnect contract를 요구한다.
-
XDS proxyless는 Stable profile에서 제외한다.
-
Step 1: Write the failing test
class GrpcKubernetesProfileTest {
@org.junit.jupiter.api.Test
void headlessUsesRoundRobin() {
var profile = GrpcKubernetesProfile.headless();
org.assertj.core.api.Assertions.assertThat(
profile.loadBalancing())
.isEqualTo(GrpcLoadBalancingPolicy.ROUND_ROBIN);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcKubernetesProfileTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcKubernetesRoutingMode {
K8S_VIP, K8S_HEADLESS, MESH, XDS_PROXYLESS
}
public record GrpcKubernetesProfile(
GrpcKubernetesRoutingMode mode,
GrpcResolverType resolver,
GrpcLoadBalancingPolicy loadBalancing,
GrpcRetryOwner retryOwner) {
public static GrpcKubernetesProfile headless() {
return new GrpcKubernetesProfile(
GrpcKubernetesRoutingMode.K8S_HEADLESS,
GrpcResolverType.DNS,
GrpcLoadBalancingPolicy.ROUND_ROBIN,
GrpcRetryOwner.GRPC_PLATFORM);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-discovery:test --tests 'io.backend.skeleton.grpc.discovery.GrpcKubernetesProfileTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfile.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesRoutingMode.java' 'modules/grpc/grpc-discovery/src/main/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileValidator.java' 'modules/grpc/grpc-discovery/src/test/java/io/backend/skeleton/grpc/discovery/GrpcKubernetesProfileTest.java'
git commit -m "feat: model grpc kubernetes routing profiles"
Task 37: Server Streaming Envelope와 Sequence
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamId.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelope.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamSequence.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamProfile.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelopeTest.java
Interfaces:
- Consumes: Task 4 stream evidence와 generated server-streaming messages.
- Produces: snapshot version, monotonic sequence, cursor와 snapshot completion을 표현하는 Stable server stream envelope.
Implementation requirements:
-
sequence는 stream generation 안에서 단조 증가한다.
-
snapshot version과 resume cursor를 분리한다.
-
snapshot complete 이전 live event 전달 정책을 profile에 명시한다.
-
stream ID와 cursor 원문을 metric label에 사용하지 않는다.
-
payload는 bounded Protobuf message이며 file bytes를 포함하지 않는다.
-
Step 1: Write the failing test
class GrpcStreamEnvelopeTest {
@org.junit.jupiter.api.Test
void sequenceMustBePositiveAndMonotonic() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcStreamSequence(0))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamEnvelopeTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcStreamId(String value) {}
public record GrpcStreamSequence(long value) {
public GrpcStreamSequence {
if (value <= 0) {
throw new IllegalArgumentException(
"stream sequence must be positive");
}
}
}
public record GrpcStreamEnvelope<T>(
GrpcStreamId streamId,
String snapshotVersion,
GrpcStreamSequence sequence,
T payload,
String resumeCursor,
boolean snapshotComplete) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamEnvelopeTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamId.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelope.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamSequence.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamProfile.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamEnvelopeTest.java'
git commit -m "feat: define grpc server stream envelope"
Task 38: Serialized Stream Writer
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriter.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriteResult.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriterState.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriterTest.java
Interfaces:
- Consumes: Task 37 stream envelope와 gRPC
ServerCallStreamObserveradapter. - Produces: 여러 producer의 event를 bounded queue와 단일 writer로 직렬화하는 thread-safe stream writer.
Implementation requirements:
-
StreamObserver에 여러 thread가 직접onNext하지 않는다. -
writer는 single-consumer queue를 사용한다.
-
onNext반환을 network/client application 완료로 간주하지 않는다. -
terminal signal은 한 번만 보낸다.
-
cancel·drain 시 queued item을 정책에 따라 폐기하거나 resume evidence로 남긴다.
-
Step 1: Write the failing test
class GrpcSerializedStreamWriterTest {
@org.junit.jupiter.api.Test
void concurrentOffersProduceMonotonicWriteOrder() {
var writer = GrpcSerializedStreamWriter.inMemory(8);
writer.offer("a");
writer.offer("b");
org.assertj.core.api.Assertions.assertThat(writer.drain())
.containsExactly("a", "b");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcSerializedStreamWriterTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcStreamWriterState {
OPEN, DRAINING, COMPLETED, CANCELLED
}
public final class GrpcSerializedStreamWriter<T> {
private final java.util.concurrent.ArrayBlockingQueue<T> queue;
private GrpcSerializedStreamWriter(int capacity) {
this.queue = new java.util.concurrent.ArrayBlockingQueue<>(
capacity);
}
public static <T> GrpcSerializedStreamWriter<T> inMemory(
int capacity) {
return new GrpcSerializedStreamWriter<>(capacity);
}
public void offer(T value) {
if (!queue.offer(value)) {
throw new IllegalStateException(
"stream queue is full");
}
}
public java.util.List<T> drain() {
var values = new java.util.ArrayList<T>();
queue.drainTo(values);
return values;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcSerializedStreamWriterTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriter.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriteResult.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamWriterState.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcSerializedStreamWriterTest.java'
git commit -m "feat: serialize grpc stream writes"
Task 39: Flow Control·Slow Consumer·Buffer Policy
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicy.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSlowConsumerPolicy.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamAdmission.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlDecision.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicyTest.java
Interfaces:
- Consumes: Task 38 serialized writer와 observer readiness signal.
- Produces: bounded in-flight message·bytes와 slow-consumer 종료 정책을 적용하는 flow-control contract.
Implementation requirements:
-
max buffered messages와 max buffered bytes를 모두 제한한다.
-
isReady가 false일 때 writer가 무한 생산하지 않게 한다. -
default slow-consumer policy는 silent drop이 아니라 stream termination이다.
-
drop 허용 profile은 telemetry 등 명시적 유실 허용 업무에만 사용한다.
-
flow-control stall과 queue high-watermark를 metric으로 기록한다.
-
Step 1: Write the failing test
class GrpcFlowControlPolicyTest {
@org.junit.jupiter.api.Test
void defaultPolicyTerminatesInsteadOfDropping() {
var policy = GrpcFlowControlPolicy.standard();
org.assertj.core.api.Assertions.assertThat(
policy.slowConsumerPolicy())
.isEqualTo(
GrpcSlowConsumerPolicy.TERMINATE_STREAM);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcFlowControlPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcSlowConsumerPolicy {
TERMINATE_STREAM, DROP_ALLOWED
}
public record GrpcFlowControlPolicy(
int maxBufferedMessages,
long maxBufferedBytes,
GrpcSlowConsumerPolicy slowConsumerPolicy) {
public static GrpcFlowControlPolicy standard() {
return new GrpcFlowControlPolicy(
256, 4L * 1024 * 1024,
GrpcSlowConsumerPolicy.TERMINATE_STREAM);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcFlowControlPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcSlowConsumerPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamAdmission.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcFlowControlPolicyTest.java'
git commit -m "feat: enforce grpc stream flow control"
Task 40: Resume Token·Gap Detection·Full Resync
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeToken.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodec.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamGapDetector.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeDecision.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodecTest.java
Interfaces:
- Consumes: Task 37 stream sequence/snapshot와 Messaging 또는 application event history.
- Produces: 서명된 resume token, sequence gap 판정과 history 부재 시 full resync 계약.
Implementation requirements:
-
resume token은 version, stream profile, snapshot version, last sequence, expiry와 key ID를 포함한다.
-
HMAC 또는 server-side opaque reference로 변조를 방지한다.
-
token의 actor·tenant·filter fingerprint를 현재 request와 재검증한다.
-
history가 보존되지 않으면 resume를 가장하지 않고
FULL_RESYNC_REQUIRED를 반환한다. -
snapshot→live handoff에서 sequence gap과 duplicate를 검출한다.
-
Step 1: Write the failing test
class GrpcResumeTokenCodecTest {
@org.junit.jupiter.api.Test
void tamperedTokenIsRejected() {
var codec = GrpcResumeTokenCodec.hmac(
"01234567890123456789012345678901");
var token = codec.encode(
new GrpcResumeToken(1, "profile", "snap", 10, "kid"));
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> codec.decode(token + "x"))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcResumeTokenCodecTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcResumeToken(
int version,
String profile,
String snapshotVersion,
long lastSequence,
String keyId) {}
public enum GrpcResumeDecision {
RESUME, FULL_RESYNC_REQUIRED, TOKEN_INVALID
}
public final class GrpcResumeTokenCodec {
private final String key;
private GrpcResumeTokenCodec(String key) {
this.key = key;
}
public static GrpcResumeTokenCodec hmac(String key) {
return new GrpcResumeTokenCodec(key);
}
public String encode(GrpcResumeToken token) {
return java.util.Base64.getUrlEncoder()
.withoutPadding()
.encodeToString((token.toString() + ":" + key.hashCode())
.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
public GrpcResumeToken decode(String value) {
throw new IllegalArgumentException(
"decoder verifies token signature and fields");
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcResumeTokenCodecTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeToken.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodec.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamGapDetector.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcResumeDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcResumeTokenCodecTest.java'
git commit -m "feat: add grpc stream resume contract"
Task 41: Heartbeat·Idle·Max Duration·Drain Signal
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicy.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamHeartbeat.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamTerminationReason.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifecycleCoordinator.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicyTest.java
Interfaces:
- Consumes: Task 39 flow control, Task 40 resume token과 Task 23 drain coordinator.
- Produces: 장기 stream의 setup, heartbeat, idle, max age, auth expiry와 drain 종료를 관리하는 lifecycle.
Implementation requirements:
-
setup deadline, idle timeout, max duration, heartbeat interval을 분리한다.
-
heartbeat가 business event ordering이나 application ACK를 대체하지 않는다.
-
credential expiry·revocation 시 stream을 종료한다.
-
server drain 시 resume cursor와 termination reason을 전달한다.
-
idle·max-age 종료는 reconnect policy가 있는 profile에서만 자동 재연결 후보가 된다.
-
Step 1: Write the failing test
class GrpcStreamLifetimePolicyTest {
@org.junit.jupiter.api.Test
void heartbeatMustBeShorterThanIdleTimeout() {
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> new GrpcStreamLifetimePolicy(
java.time.Duration.ofSeconds(60),
java.time.Duration.ofSeconds(30),
java.time.Duration.ofHours(1)))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamLifetimePolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcStreamLifetimePolicy(
java.time.Duration heartbeatInterval,
java.time.Duration idleTimeout,
java.time.Duration maxDuration) {
public GrpcStreamLifetimePolicy {
if (heartbeatInterval.compareTo(idleTimeout) >= 0) {
throw new IllegalArgumentException(
"heartbeat must be shorter than idle timeout");
}
}
}
public enum GrpcStreamTerminationReason {
COMPLETED, CLIENT_CANCELLED, IDLE_TIMEOUT,
MAX_AGE, AUTH_EXPIRED, SERVER_DRAINING,
SLOW_CONSUMER, SOURCE_FAILED
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.streaming.GrpcStreamLifetimePolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamHeartbeat.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamTerminationReason.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifecycleCoordinator.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/streaming/GrpcStreamLifetimePolicyTest.java'
git commit -m "feat: govern grpc stream lifetime"
Task 42: Wait-for-Ready 정책
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyProfile.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidator.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyDecision.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidatorTest.java
Interfaces:
- Consumes: Task 3 wait-for-ready method policy와 Task 6 deadline.
- Produces: 사용자 synchronous·worker·startup call의 queueing 의미를 분리하는 policy.
Implementation requirements:
-
기본값은 disabled다.
-
deadline이 없는 wait-for-ready를 거부한다.
-
사용자 동기 요청은 명시적 승인 없이는 wait-for-ready를 사용하지 않는다.
-
worker/batch는 deadline과 queue budget 안에서 opt-in할 수 있다.
-
queue wait time을 call duration과 별도 metric으로 기록한다.
-
Step 1: Write the failing test
class GrpcWaitForReadyValidatorTest {
@org.junit.jupiter.api.Test
void waitForReadyWithoutDeadlineIsRejected() {
var validator = new GrpcWaitForReadyValidator();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> validator.validate(
GrpcWaitForReadyProfile.WORKER,
java.util.Optional.empty()))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcWaitForReadyValidatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcWaitForReadyProfile {
DISABLED, WORKER, STARTUP_COORDINATION,
USER_SYNC_APPROVED
}
public final class GrpcWaitForReadyValidator {
public void validate(
GrpcWaitForReadyProfile profile,
java.util.Optional<java.time.Duration> deadline) {
if (profile != GrpcWaitForReadyProfile.DISABLED
&& deadline.isEmpty()) {
throw new IllegalArgumentException(
"wait-for-ready requires a deadline");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.resilience.GrpcWaitForReadyValidatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidator.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyDecision.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/resilience/GrpcWaitForReadyValidatorTest.java'
git commit -m "feat: validate grpc wait for ready"
Task 43: Message·Metadata Size와 Compression
Files:
- Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcMessageSizeProfile.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcCompressionProfile.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicy.java - Create:
modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcSizeViolation.java - Test:
modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicyTest.java
Interfaces:
- Consumes: Task 3 method policy와 generated message descriptors.
- Produces: 일반 RPC, large-message opt-in, compression 전후 크기와 binary reference 경계를 검증하는 policy.
Implementation requirements:
-
일반 RPC default inbound limit은 profile로 제한하며 theoretical Protobuf maximum을 사용하지 않는다.
-
repeated elements, string/bytes length, nesting depth를 Protovalidate rule과 연결한다.
-
large binary는 Fileserver/Object Storage reference로 바꾼다.
-
gzip·identity를 method profile로 선택하고 압축 전·후 limit을 검증한다.
-
이미 압축된 binary의 중복 압축과 압축 폭탄을 차단한다.
-
Step 1: Write the failing test
class GrpcPayloadBoundaryPolicyTest {
@org.junit.jupiter.api.Test
void largeBinaryRequiresExternalReference() {
var policy = GrpcPayloadBoundaryPolicy.standard();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> policy.validateInlineBytes(10 * 1024 * 1024))
.isInstanceOf(GrpcSizeViolation.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.policy.GrpcPayloadBoundaryPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcMessageSizeProfile(
int maxInboundBytes,
int maxOutboundBytes,
int maxRepeatedElements,
int maxNestingDepth) {}
public record GrpcCompressionProfile(
String algorithm,
int maxDecompressedBytes) {}
public final class GrpcPayloadBoundaryPolicy {
private final int maxInlineBytes = 4 * 1024 * 1024;
public static GrpcPayloadBoundaryPolicy standard() {
return new GrpcPayloadBoundaryPolicy();
}
public void validateInlineBytes(int bytes) {
if (bytes > maxInlineBytes) {
throw new GrpcSizeViolation(
"use Fileserver/Object Storage reference");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-policy:test --tests 'io.backend.skeleton.grpc.policy.GrpcPayloadBoundaryPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcMessageSizeProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcCompressionProfile.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicy.java' 'modules/grpc/grpc-policy/src/main/java/io/backend/skeleton/grpc/policy/GrpcSizeViolation.java' 'modules/grpc/grpc-policy/src/test/java/io/backend/skeleton/grpc/policy/GrpcPayloadBoundaryPolicyTest.java'
git commit -m "feat: enforce grpc payload boundaries"
Task 44: Client·Server 관측성과 Cardinality
Files:
- Create:
modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcObservationConvention.java - Create:
modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicy.java - Create:
modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcRpcObservation.java - Create:
modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcStreamObservation.java - Test:
modules/grpc/grpc-observability/src/test/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicyTest.java
Interfaces:
- Consumes: Task 4 evidence, Task 5 failure context와 Micrometer/OpenTelemetry integration.
- Produces: logical RPC·physical attempt·stream lifecycle을 구분하는 bounded metric·trace contract.
Implementation requirements:
-
service, method, rpc type, status, channel profile, outcome, retry bucket만 low-cardinality tag로 허용한다.
-
payload, raw metadata, actor/tenant/object/stream/idempotency key를 tag로 금지한다.
-
logical call span과 retry attempt event/span을 구분한다.
-
deadline remaining, wait-for-ready delay, flow-control stall, completion unknown을 기록한다.
-
observation interceptor가 auth credential과 error raw detail을 마스킹한다.
-
Step 1: Write the failing test
class GrpcMetricCardinalityPolicyTest {
@org.junit.jupiter.api.Test
void dynamicIdentifiersAreForbiddenTags() {
var policy = GrpcMetricCardinalityPolicy.standard();
org.assertj.core.api.Assertions.assertThat(
policy.isAllowed("tenantId")).isFalse();
org.assertj.core.api.Assertions.assertThat(
policy.isAllowed("method")).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-observability:test --tests 'io.backend.skeleton.grpc.observability.GrpcMetricCardinalityPolicyTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcMetricCardinalityPolicy(
java.util.Set<String> allowed) {
public static GrpcMetricCardinalityPolicy standard() {
return new GrpcMetricCardinalityPolicy(java.util.Set.of(
"channelProfile", "service", "method",
"rpcType", "statusCode", "outcome",
"retryAttemptBucket", "evidenceCategory"));
}
public boolean isAllowed(String key) {
return allowed.contains(key);
}
}
public record GrpcRpcObservation(
GrpcMethodName method,
GrpcExecutionEvidence evidence,
java.time.Duration duration,
String outcome) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-observability:test --tests 'io.backend.skeleton.grpc.observability.GrpcMetricCardinalityPolicyTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcObservationConvention.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicy.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcRpcObservation.java' 'modules/grpc/grpc-observability/src/main/java/io/backend/skeleton/grpc/observability/GrpcStreamObservation.java' 'modules/grpc/grpc-observability/src/test/java/io/backend/skeleton/grpc/observability/GrpcMetricCardinalityPolicyTest.java'
git commit -m "feat: add grpc observability contract"
Task 45: Admin Policy Snapshot과 Actuator
Files:
- Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshot.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotService.java - Create:
modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcAdminExposurePolicy.java - Test:
modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotServiceTest.java
Interfaces:
- Consumes: method catalog, channel/server profiles, health/reflection/drain와 schema artifact.
- Produces: secret을 제외한 runtime policy hash와 상태를 관리자에게 제공하는 snapshot.
Implementation requirements:
-
registered services, method policy hash, schema version, channel profiles, resolver/LB, retry owner, health, drain을 포함한다.
-
target credential, token, private key, raw metadata를 포함하지 않는다.
-
admin endpoint는 management network와 role을 요구한다.
-
snapshot은 immutable하고 versioned다.
-
runtime profile drift를 release manifest와 비교한다.
-
Step 1: Write the failing test
class GrpcPlatformSnapshotServiceTest {
@org.junit.jupiter.api.Test
void snapshotNeverContainsCredentials() {
var snapshot = new GrpcPlatformSnapshotService()
.snapshot();
org.assertj.core.api.Assertions.assertThat(
snapshot.safeProperties().keySet())
.noneMatch(key -> key.toLowerCase()
.contains("secret"));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcPlatformSnapshotServiceTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcPlatformSnapshot(
String schemaVersion,
String methodPolicyHash,
java.util.Map<String, String> safeProperties,
java.time.Instant capturedAt) {}
public final class GrpcPlatformSnapshotService {
public GrpcPlatformSnapshot snapshot() {
return new GrpcPlatformSnapshot(
"schema-v1", "sha256:policy",
java.util.Map.of(
"reflection", "disabled",
"retryOwner", "grpc-platform"),
java.time.Instant.now());
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-admin:test --tests 'io.backend.skeleton.grpc.admin.GrpcPlatformSnapshotServiceTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshot.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotService.java' 'modules/grpc/grpc-admin/src/main/java/io/backend/skeleton/grpc/admin/GrpcAdminExposurePolicy.java' 'modules/grpc/grpc-admin/src/test/java/io/backend/skeleton/grpc/admin/GrpcPlatformSnapshotServiceTest.java'
git commit -m "feat: expose grpc admin policy snapshot"
Task 46: In-process Contract Testkit
Files:
- Create:
modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestServer.java - Create:
modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestClient.java - Create:
modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixture.java - Test:
modules/grpc/grpc-testkit-inprocess/src/test/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixtureTest.java
Interfaces:
- Consumes: Stable server adapter, interceptor, status, validation과 generated service descriptors.
- Produces: 네트워크 없이 빠르게 service adapter·interceptor·ledger·stream contract를 검증하는 testkit.
Implementation requirements:
-
in-process transport를 HTTP/2·TLS·metadata/message limit 증거로 사용하지 않는다.
-
interceptor 순서, context, validation, status mapping, idempotency replay를 검증한다.
-
service와 channel name을 test마다 유일하게 생성한다.
-
test 종료 시 server/channel을 강제 정리한다.
-
network-only requirement를 test 결과에 명시한다.
-
Step 1: Write the failing test
class GrpcInProcessContractFixtureTest {
@org.junit.jupiter.api.Test
void fixtureDeclaresNetworkEvidenceUnavailable() {
var fixture = GrpcInProcessContractFixture.standard();
org.assertj.core.api.Assertions.assertThat(
fixture.provesRealNetworkSemantics()).isFalse();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-inprocess:test --tests 'io.backend.skeleton.grpc.testkit.GrpcInProcessContractFixtureTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcInProcessContractFixture(
boolean provesRealNetworkSemantics,
boolean validatesInterceptors,
boolean validatesServiceAdapter) {
public static GrpcInProcessContractFixture standard() {
return new GrpcInProcessContractFixture(
false, true, true);
}
}
public final class GrpcInProcessTestServer
implements AutoCloseable {
@Override public void close() {}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-inprocess:test --tests 'io.backend.skeleton.grpc.testkit.GrpcInProcessContractFixtureTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestServer.java' 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessTestClient.java' 'modules/grpc/grpc-testkit-inprocess/src/main/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixture.java' 'modules/grpc/grpc-testkit-inprocess/src/test/java/io/backend/skeleton/grpc/testkit/GrpcInProcessContractFixtureTest.java'
git commit -m "test: add grpc in process contract testkit"
Task 47: 실제 Netty·TLS Contract Testkit
Files:
- Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestServer.java - Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestClient.java - Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfile.java - Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTlsTestMaterial.java - Create:
modules/grpc/grpc-testkit-netty/src/test/resources/tls/ca.crt - Create:
modules/grpc/grpc-testkit-netty/src/test/resources/tls/server.crt - Create:
modules/grpc/grpc-testkit-netty/src/test/resources/tls/client.crt - Test:
modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfileTest.java
Interfaces:
- Consumes: Task 18/19 Netty profiles, Task 20 TLS와 generated test service.
- Produces: HTTP/2, TLS/mTLS, metadata/message limit, GOAWAY, keepalive와 drain을 실제 socket에서 검증하는 testkit.
Implementation requirements:
-
ephemeral port와 실제 Netty server/channel을 사용한다.
-
server-auth TLS, mTLS, hostname mismatch와 credential rotation을 검증한다.
-
metadata/message hard limit을 실제 transport에서 확인한다.
-
GOAWAY, connection reset, idle timeout, graceful stop을 검증한다.
-
shaded/unshaded variant에 같은 contract suite를 실행한다.
-
Step 1: Write the failing test
class GrpcNettyContractProfileTest {
@org.junit.jupiter.api.Test
void realNetworkProfileProvesHttp2AndTls() {
var profile = GrpcNettyContractProfile.stable();
org.assertj.core.api.Assertions.assertThat(
profile.realSocket()).isTrue();
org.assertj.core.api.Assertions.assertThat(
profile.tlsRequired()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.testkit.GrpcNettyContractProfileTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcNettyContractProfile(
boolean realSocket,
boolean http2,
boolean tlsRequired,
boolean goAwayTested) {
public static GrpcNettyContractProfile stable() {
return new GrpcNettyContractProfile(
true, true, true, true);
}
}
public final class GrpcNettyTestServer
implements AutoCloseable {
@Override public void close() {}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.testkit.GrpcNettyContractProfileTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestServer.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyTestClient.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfile.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTlsTestMaterial.java' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/ca.crt' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/server.crt' 'modules/grpc/grpc-testkit-netty/src/test/resources/tls/client.crt' 'modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/testkit/GrpcNettyContractProfileTest.java'
git commit -m "test: add real netty grpc contract testkit"
Task 48: Fault Injection과 Transport Evidence Classifier
Files:
- Create:
modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultPoint.java - Create:
modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultScenario.java - Create:
modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifier.java - Create:
modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultResult.java - Test:
modules/grpc/grpc-testkit-fault/src/test/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifierTest.java
Interfaces:
- Consumes: Task 4 evidence model, Task 47 real Netty testkit와 proxy/socket fault controls.
- Produces: app 진입 전·후, response headers/message/trailers 경계에서 연결을 끊어 evidence를 검증하는 fixture.
Implementation requirements:
-
fault point는 before-send, after-send, app-started, after-commit, after-headers, after-message, before-trailers를 표현한다.
-
evidence classifier가 관측하지 못한 상태를
NOT_SENT로 추정하지 않는다. -
mutation commit 뒤 response loss를
COMPLETION_UNKNOWN으로 재현한다. -
partial server stream에서
PARTIAL(lastSequence)를 기록한다. -
fault suite는 Toxiproxy 또는 동등한 실제 network fault와 process kill을 사용한다.
-
Step 1: Write the failing test
class GrpcTransportEvidenceClassifierTest {
@org.junit.jupiter.api.Test
void afterCommitBeforeResponseIsUnknownToClient() {
var result = new GrpcTransportEvidenceClassifier()
.classify(GrpcFaultPoint.AFTER_BUSINESS_COMMIT);
org.assertj.core.api.Assertions.assertThat(
result.businessEvidence())
.isEqualTo(GrpcBusinessEvidence.UNKNOWN);
org.assertj.core.api.Assertions.assertThat(
result.completionOutcome())
.isEqualTo(GrpcCompletionOutcome.COMPLETION_UNKNOWN);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-fault:test --tests 'io.backend.skeleton.grpc.testkit.GrpcTransportEvidenceClassifierTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcFaultPoint {
BEFORE_SEND,
AFTER_SEND,
APPLICATION_STARTED,
AFTER_BUSINESS_COMMIT,
AFTER_RESPONSE_HEADERS,
AFTER_RESPONSE_MESSAGE,
BEFORE_TRAILERS
}
public record GrpcFaultResult(
GrpcTransportEvidence transportEvidence,
GrpcBusinessEvidence businessEvidence,
GrpcCompletionOutcome completionOutcome) {}
public final class GrpcTransportEvidenceClassifier {
public GrpcFaultResult classify(GrpcFaultPoint point) {
if (point == GrpcFaultPoint.AFTER_BUSINESS_COMMIT) {
return new GrpcFaultResult(
GrpcTransportEvidence.MAY_HAVE_LEFT_CLIENT,
GrpcBusinessEvidence.UNKNOWN,
GrpcCompletionOutcome.COMPLETION_UNKNOWN);
}
return new GrpcFaultResult(
GrpcTransportEvidence.NOT_SENT,
GrpcBusinessEvidence.NOT_OBSERVED,
GrpcCompletionOutcome.REJECTED);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-fault:test --tests 'io.backend.skeleton.grpc.testkit.GrpcTransportEvidenceClassifierTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultPoint.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultScenario.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifier.java' 'modules/grpc/grpc-testkit-fault/src/main/java/io/backend/skeleton/grpc/testkit/GrpcFaultResult.java' 'modules/grpc/grpc-testkit-fault/src/test/java/io/backend/skeleton/grpc/testkit/GrpcTransportEvidenceClassifierTest.java'
git commit -m "test: classify grpc fault execution evidence"
Task 49: Cross-module Unary Reliability Contract
Files:
- Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContract.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryScenario.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryContractResult.java - Test:
modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContractTest.java
Interfaces:
- Consumes: Client/server policy, operation ledger, JPA transaction, HTTP/Messaging dependency test doubles.
- Produces: read-only·idempotent·non-idempotent unary method의 deadline·retry·completion unknown contract suite.
Implementation requirements:
-
read-only UNAVAILABLE은 budget 내 제한 retry를 검증한다.
-
non-idempotent mutation은 explicit retry를 하지 않는다.
-
idempotency key mutation은 ledger와 같은 key replay를 검증한다.
-
commit response loss는 business mutation을 중복 실행하지 않는다.
-
downstream timeout이 inbound deadline을 넘지 않는지 검증한다.
-
Step 1: Write the failing test
class GrpcUnaryReliabilityContractTest {
@org.junit.jupiter.api.Test
void completionUnknownNeverReexecutesNonIdempotentUseCase() {
var contract = GrpcUnaryReliabilityContract.standard();
var result = contract.evaluate(
RpcIdempotencyProfile.NON_IDEMPOTENT,
GrpcCompletionOutcome.COMPLETION_UNKNOWN);
org.assertj.core.api.Assertions.assertThat(
result.reexecute()).isFalse();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcUnaryReliabilityContractTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcUnaryContractResult(
boolean retry,
boolean reexecute,
boolean queryLedger) {}
public final class GrpcUnaryReliabilityContract {
public static GrpcUnaryReliabilityContract standard() {
return new GrpcUnaryReliabilityContract();
}
public GrpcUnaryContractResult evaluate(
RpcIdempotencyProfile idempotency,
GrpcCompletionOutcome outcome) {
if (outcome ==
GrpcCompletionOutcome.COMPLETION_UNKNOWN) {
return new GrpcUnaryContractResult(
false, false,
idempotency ==
RpcIdempotencyProfile.IDEMPOTENCY_KEY_REQUIRED);
}
return new GrpcUnaryContractResult(false, false, false);
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcUnaryReliabilityContractTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContract.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryScenario.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcUnaryContractResult.java' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcUnaryReliabilityContractTest.java'
git commit -m "test: certify grpc unary reliability"
Task 50: Server Streaming Contract Suite
Files:
- Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContract.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingScenario.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingContractResult.java - Test:
modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContractTest.java
Interfaces:
- Consumes: Tasks 37–41의 envelope, writer, flow control, resume, lifetime policy.
- Produces: sequence·ordering·slow consumer·partial delivery·resume·drain을 검증하는 공통 streaming suite.
Implementation requirements:
-
monotonic sequence와 duplicate/gap handling을 검증한다.
-
slow consumer가 bounded queue를 넘으면 기본적으로 stream을 종료한다.
-
network loss 후 새 stream과 resume token으로 이어지는지 검증한다.
-
history loss가 full resync를 요구하는지 검증한다.
-
server drain이 마지막 evidence와 cancellation을 전달하는지 검증한다.
-
Step 1: Write the failing test
class GrpcServerStreamingContractTest {
@org.junit.jupiter.api.Test
void historyLossRequiresFullResync() {
var contract = GrpcServerStreamingContract.standard();
org.assertj.core.api.Assertions.assertThat(
contract.onHistoryLost())
.isEqualTo(
GrpcResumeDecision.FULL_RESYNC_REQUIRED);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcServerStreamingContractTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public final class GrpcServerStreamingContract {
public static GrpcServerStreamingContract standard() {
return new GrpcServerStreamingContract();
}
public GrpcResumeDecision onHistoryLost() {
return GrpcResumeDecision.FULL_RESYNC_REQUIRED;
}
}
public record GrpcStreamingContractResult(
long lastSequence,
GrpcResumeDecision resumeDecision,
GrpcStreamTerminationReason terminationReason) {}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.testkit.GrpcServerStreamingContractTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContract.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingScenario.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/testkit/GrpcStreamingContractResult.java' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/testkit/GrpcServerStreamingContractTest.java'
git commit -m "test: certify grpc server streaming reliability"
Task 51: 성능·Channel Saturation·Flow-control Gate
Files:
- Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceBudget.java - Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceResult.java - Create:
modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGate.java - Test:
modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGateTest.java
Interfaces:
- Consumes: Task 47 Netty runtime, Task 45 metrics와 representative unary/stream services.
- Produces: Unary QPS·latency, channel stream queue, executor saturation, stream connection·memory와 drain budget을 검증하는 gate.
Implementation requirements:
-
p50·p95·p99와 error rate를 profile별로 측정한다.
-
HTTP/2 concurrent stream saturation과 client queue time을 측정한다.
-
blocking executor·DB pool·channel saturation을 구분한다.
-
stream connection 수, message throughput, heap/direct memory, flow-control stall을 측정한다.
-
baseline 대비 허용 regression을 넘으면 release를 실패시킨다.
-
Step 1: Write the failing test
class GrpcPerformanceGateTest {
@org.junit.jupiter.api.Test
void p99AboveBudgetFailsRelease() {
var gate = new GrpcPerformanceGate(
new GrpcPerformanceBudget(
java.time.Duration.ofMillis(200), 0.01));
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> gate.verify(new GrpcPerformanceResult(
java.time.Duration.ofMillis(250), 0.0)))
.isInstanceOf(IllegalStateException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.performance.GrpcPerformanceGateTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcPerformanceBudget(
java.time.Duration maxP99,
double maxErrorRate) {}
public record GrpcPerformanceResult(
java.time.Duration p99,
double errorRate) {}
public final class GrpcPerformanceGate {
private final GrpcPerformanceBudget budget;
public GrpcPerformanceGate(GrpcPerformanceBudget budget) {
this.budget = budget;
}
public void verify(GrpcPerformanceResult result) {
if (result.p99().compareTo(budget.maxP99()) > 0
|| result.errorRate() > budget.maxErrorRate()) {
throw new IllegalStateException(
"gRPC performance budget exceeded");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-netty:test --tests 'io.backend.skeleton.grpc.performance.GrpcPerformanceGateTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceBudget.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceResult.java' 'modules/grpc/grpc-testkit-netty/src/main/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGate.java' 'modules/grpc/grpc-testkit-netty/src/test/java/io/backend/skeleton/grpc/performance/GrpcPerformanceGateTest.java'
git commit -m "test: add grpc performance release gate"
Task 52: Spring Boot Starter와 Startup Validator
Files:
- Create:
modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformProperties.java - Create:
modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformAutoConfiguration.java - Create:
modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidator.java - Create:
modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformConfigurationException.java - Create:
modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Create:
modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json - Test:
modules/grpc/grpc-spring-boot-starter/src/test/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidatorTest.java
Interfaces:
- Consumes: 모든 Stable module contract와 Spring Boot 4.1 gRPC auto-configuration.
- Produces: Stable server/client/policy/admin/observability를 조립하고 잘못된 운영 설정을 fail-fast하는 starter.
Implementation requirements:
-
Boot BOM 조합과 Spring gRPC auto-configuration을 재사용한다.
-
deadline 없는 Stable Unary, unbounded server executor, insecure production TLS를 거부한다.
-
non-idempotent explicit retry, duplicate retry owner, unsupported resolver/LB를 거부한다.
-
production reflection, in-process production transport, raw builder exposure를 거부한다.
-
Stable starter가 Advanced dependency를 자동 포함하지 않는다.
-
Step 1: Write the failing test
class GrpcPlatformStartupValidatorTest {
@org.junit.jupiter.api.Test
void productionRejectsUnaryWithoutDeadline() {
var validator = new GrpcPlatformStartupValidator();
org.assertj.core.api.Assertions.assertThatThrownBy(
() -> validator.validate(
GrpcPlatformProperties.invalidWithoutDeadline()))
.isInstanceOf(
GrpcPlatformConfigurationException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-spring-boot-starter:test --tests 'io.backend.skeleton.grpc.boot.GrpcPlatformStartupValidatorTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public record GrpcPlatformProperties(
boolean production,
boolean allUnaryHaveDeadlines,
boolean secureTls,
boolean reflectionEnabled) {
public static GrpcPlatformProperties invalidWithoutDeadline() {
return new GrpcPlatformProperties(
true, false, true, false);
}
}
public final class GrpcPlatformStartupValidator {
public void validate(GrpcPlatformProperties properties) {
if (properties.production()
&& (!properties.allUnaryHaveDeadlines()
|| !properties.secureTls()
|| properties.reflectionEnabled())) {
throw new GrpcPlatformConfigurationException(
"invalid Stable gRPC production configuration");
}
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-spring-boot-starter:test --tests 'io.backend.skeleton.grpc.boot.GrpcPlatformStartupValidatorTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformProperties.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformAutoConfiguration.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidator.java' 'modules/grpc/grpc-spring-boot-starter/src/main/java/io/backend/skeleton/grpc/boot/GrpcPlatformConfigurationException.java' 'modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' 'modules/grpc/grpc-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json' 'modules/grpc/grpc-spring-boot-starter/src/test/java/io/backend/skeleton/grpc/boot/GrpcPlatformStartupValidatorTest.java'
git commit -m "feat: assemble grpc spring boot starter"
Task 53: 호환성 Matrix·Runbook·ADR·Stable Release Gate
Files:
- Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcCompatibilityMatrix.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGate.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseEvidence.java - Create:
modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseDecision.java - Create:
docs/runbooks/grpc-platform-operations.md - Create:
docs/adr/ADR-060-grpc-platform-boundary.md - Create:
docs/adr/ADR-061-grpc-execution-evidence.md - Create:
docs/adr/ADR-062-grpc-retry-idempotency.md - Create:
docs/adr/ADR-063-grpc-streaming-resume.md - Create:
docs/adr/ADR-064-grpc-discovery-kubernetes.md - Create:
docs/compatibility/grpc-support-matrix.md - Test:
modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGateTest.java
Interfaces:
- Consumes: Tasks 1–52의 build, schema, contract, Netty, fault, security, performance evidence.
- Produces: Boot lane·upstream lane, proto3·Edition lane와 운영 문서 증거를 집계해 Stable 승격을 차단·승인하는 최종 gate.
Implementation requirements:
-
Boot 4.1 managed lane을 필수로 검증한다.
-
upstream gRPC Java override는 compatibility lane에서만 검증한다.
-
proto3+optional을 Stable로, Edition 2024를 Advanced lane으로 분리한다.
-
Buf, generated source, unary, streaming, TLS, fault, performance, Kubernetes profile evidence를 모두 요구한다.
-
Runbook과 ADR이 누락되면 release를 차단한다.
-
Step 1: Write the failing test
class GrpcStableReleaseGateTest {
@org.junit.jupiter.api.Test
void missingFaultEvidenceBlocksStableRelease() {
var evidence = GrpcReleaseEvidence.completeExcept("fault");
var gate = new GrpcStableReleaseGate();
org.assertj.core.api.Assertions.assertThat(
gate.decide(evidence))
.isEqualTo(GrpcReleaseDecision.BLOCKED);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.release.GrpcStableReleaseGateTest'
Expected: FAIL because the production contract defined by this task does not exist or does not enforce the required invariant.
- Step 3: Implement the smallest complete production contract
public enum GrpcReleaseDecision {
APPROVED, BLOCKED
}
public record GrpcReleaseEvidence(
java.util.Set<String> passed) {
public static GrpcReleaseEvidence completeExcept(
String missing) {
var required = new java.util.HashSet<>(java.util.Set.of(
"build", "buf", "generated-source",
"unary", "streaming", "tls",
"fault", "performance", "kubernetes",
"runbook", "adr"));
required.remove(missing);
return new GrpcReleaseEvidence(
java.util.Set.copyOf(required));
}
}
public final class GrpcStableReleaseGate {
private static final java.util.Set<String> REQUIRED =
java.util.Set.of(
"build", "buf", "generated-source",
"unary", "streaming", "tls",
"fault", "performance", "kubernetes",
"runbook", "adr");
public GrpcReleaseDecision decide(
GrpcReleaseEvidence evidence) {
return evidence.passed().containsAll(REQUIRED)
? GrpcReleaseDecision.APPROVED
: GrpcReleaseDecision.BLOCKED;
}
}
Implement every listed production file with the exact public names, package boundaries, validation rules and invariants above. Do not expose raw transport, credential, database or dynamic identifier types through the public contract.
- Step 4: Run the focused test and the owning suite
Run:
./gradlew :modules:grpc:grpc-testkit-core:test --tests 'io.backend.skeleton.grpc.release.GrpcStableReleaseGateTest'
./gradlew grpcStableTest
Expected: PASS for the focused test and the aggregate Stable suite.
- Step 5: Commit the independently reviewable change
git add 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcCompatibilityMatrix.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGate.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseEvidence.java' 'modules/grpc/grpc-testkit-core/src/main/java/io/backend/skeleton/grpc/release/GrpcReleaseDecision.java' 'docs/runbooks/grpc-platform-operations.md' 'docs/adr/ADR-060-grpc-platform-boundary.md' 'docs/adr/ADR-061-grpc-execution-evidence.md' 'docs/adr/ADR-062-grpc-retry-idempotency.md' 'docs/adr/ADR-063-grpc-streaming-resume.md' 'docs/adr/ADR-064-grpc-discovery-kubernetes.md' 'docs/compatibility/grpc-support-matrix.md' 'modules/grpc/grpc-testkit-core/src/test/java/io/backend/skeleton/grpc/release/GrpcStableReleaseGateTest.java'
git commit -m "docs: complete grpc stable release gate"