Skip to main content

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…

3 viewsAbout 3 min read
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

Public request

The browser calls the Java API through a stable REST contract.

Internal validation

Java fixes size, authorization, and request ID constraints.

Bounded RPC

A protobuf request crosses to the Python engine with a deadline.

Safe degradation

A fallback runs only after failure type and idempotency are known.

Contract essentials

  • Synthesize accepts 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

  1. Generated code in Java and Python uses the same proto source.
  2. Deadline, provider, and disabled-server failures converge to safe error types and REST fallback.
  3. 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.
  4. GetCapabilities and /health/capabilities reflect the actual server lifecycle.
  5. 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

Terms in this content

More in backend

All in this category →

Was this article helpful?