Fixing the Java-Python voice boundary with a gRPC contract
The Java API and Python voice engine are separate processes, so a protobuf contract makes change and failure more explicit than an untyped internal call. voice/v1/voice.proto defines synthesis, the voice catalog, and ca…
Table of contents
The Java API and Python voice engine are separate processes, so a protobuf contract makes change and failure more explicit than an untyped internal call. voice/v1/voice.proto defines synthesis, the voice catalog, and capabilities at one versioned boundary, while the Java client enforces a request deadline.
One synthesis request across the boundary
The browser calls the Java API through a stable REST contract.
Java fixes size, authorization, and request ID constraints.
A protobuf request crosses to the Python engine with a deadline.
A fallback runs only after failure type and idempotency are known.
Contract essentials
Synthesizeaccepts provider, text, voice, and request ID, and returns audio bytes, content type, duration, and provider.- The Python server bounds text, request IDs, and message size. Provider bodies and input text are not copied into public errors.
- The Java blocking stub runs on a bounded elastic executor. When gRPC is disabled or fails, the existing REST TTS boundary is an explicit fallback.
- The public REST API remains. gRPC is an internal service contract, not a reason to force every API onto gRPC.
Separate the transport switch from cost approval
A ready gRPC transport does not authorize paid synthesis. When public REST, internal gRPC, and fallback can reach the same provider, every path must share one cost-approval condition.
effective voice = transport enabled AND server TTS approved
| Path | Local condition | Shared condition |
|---|---|---|
| Public REST | HTTP endpoint enabled | Server TTS cost approval |
| Internal gRPC | gRPC server enabled | Server TTS cost approval |
| REST fallback | Fallback target healthy | Server TTS cost approval and idempotency |
Keep both producer and consumer defaults false in Compose. Enabling only a transport flag must not expose a capability or call a provider. A fallback must recheck the same gate and never widen the authority of its primary path.
Completion criteria
- Generated code in Java and Python uses the same proto source.
- Deadline, provider, and disabled-server failures converge to safe error types and REST fallback.
- When Spring MDC has a request ID, it is forwarded as protobuf
request_id; it is correlation data, not a high-cardinality log key containing user input. GetCapabilitiesand/health/capabilitiesreflect the actual server lifecycle.- Effective Compose configuration keeps transport and cost gates closed by default, and no public, internal, or fallback path calls the provider.
Boundary decision table
| Call | Suitable boundary | Why | Required guard |
|---|---|---|---|
| Public browser API | REST | Cache, debugging, compatibility | Auth and rate limit |
| Short internal RPC | Unary gRPC | Types and deadline | Size bound and fallback |
| Long generation work | Job or queue | Separate request lifetime | Idempotency and status |
| Real-time media | WebRTC | Media transport | Cost gate and session cap |
browser ─REST─▶ Java API ─gRPC(deadline)─▶ Python engine
└─failure─▶ bounded REST fallback
A fallback must not blindly execute the same expensive request twice. No gRPC response does not prove that synthesis failed, so check the request ID and provider idempotency contract before retrying.
Related course: Close gRPC and WebRTC boundaries with cost guards