Contracts & the catalog
Versioned Avro and Protobuf contracts in a shared contracts module and Apicurio registry, and why a discovery catalog is a mesh requirement even though it isn't built yet.
Each service is a data product, but other domains can use it only if they can find
it, understand its shape, and trust that it won’t change without warning. This chapter covers the two mechanisms this project uses for that:
a shared contracts module holding every service’s Avro and Protobuf
definitions as its single source of truth, and Apicurio Schema Registry
enforcing those Avro contracts at runtime. It closes with what is not yet
built, a discovery catalog, and why that gap matters.
The code is in contracts. There’s no standalone demo script for the registry itself; demo-kafka.sh exercises it as part of proving the Kafka data plane, and this chapter’s cross-check comes from there.
Figure 4.1 shows the chapter’s scope. Every protocol surface in the mesh carries its own contract:
| Protocol | Contract type |
|---|---|
| REST | OpenAPI document |
| gRPC | .proto file |
| GraphQL | SDL schema |
| Kafka | Avro schema |
Apicurio Registry is the one home for all of them regardless of format, with
OpenMetadata sitting downstream to turn those contracts plus the running
Postgres schemas and Kafka topics into a lineage graph. That target picture
is wider than what this chapter’s code wires up today; the sections below separate
running enforcement from the conceptual target, starting with the two contract types
contracts owns.
One module, two contract types
contracts is a plain Maven module — framework-agnostic, no Quarkus
dependency at all, so any JVM service (or build) can depend on it. It holds
two different kinds of contract, generated by two different plugins, for two
different protocols:
examples/contracts/
src/main/avro/
order-placed.avsc # capstone.order.v1.OrderPlaced
payment-captured.avsc # capstone.payment.v1.PaymentCaptured
shipment-dispatched.avsc # capstone.shipping.v1.ShipmentDispatched
src/main/proto/
capstone/inventory/v1/inventory.proto
The Avro schemas are the event contracts. Here’s order-placed.avsc in full:
{
"type": "record",
"name": "OrderPlaced",
"namespace": "capstone.order.v1",
"doc": "Emitted by order-service after an order is durably persisted.",
"fields": [
{ "name": "event_type", "type": "string" },
{ "name": "order_id", "type": "string" },
{ "name": "customer_id", "type": "string" },
{ "name": "item_sku", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "amount", "type": "string" },
{ "name": "status", "type": "string" },
{ "name": "created_at", "type": "string" }
]
}
name + namespace together give every record a fully qualified identity
(capstone.order.v1.OrderPlaced) that doubles as its Apicurio artifact
identity and its generated Java package, so the three stay in lockstep by
construction. amount is a string, not Avro’s decimal logical type: the schema’s
doc field calls this “a first-slice simplification” and names the decimal logical type
as the later refinement. Anyone extending this contract should treat it as a known
shortcut.
The org.apache.avro:avro-maven-plugin’s schema goal, bound to Maven’s
generate-sources phase, turns each .avsc into a generated
SpecificRecord Java class under target/generated-sources/avro — that’s
the concrete OrderPlaced type OrderEventProducer builds with
OrderPlaced.newBuilder() in the previous chapter. The record is never
hand-written; the .avsc is the one source of truth, and the Java class is
a build artifact of it.
The Protobuf side is a single .proto for the one synchronous cross-service
call in this project:
syntax = "proto3";
package capstone.inventory.v1;
service InventoryService {
rpc CheckStock(CheckStockRequest) returns (CheckStockResponse);
}
message CheckStockRequest {
string sku = 1;
int32 quantity = 2;
}
message CheckStockResponse {
bool available = 1;
int32 quantity_on_hand = 2;
}
The contracts module does not generate gRPC stubs itself — it packages
inventory.proto as a plain resource so it ships inside the jar.
Each consuming service (order-service
as a client, inventory-service as a server) adds the quarkus-grpc
extension and points quarkus.generate-code.grpc.scan-for-proto at this
module’s Maven coordinates:
quarkus.generate-code.grpc.scan-for-proto=com.patterncatalyst.datamesh:contracts
That property is what makes the InventoryService Mutiny interface and the
CheckStockRequest/CheckStockResponse builders used throughout the
previous chapter appear under each service’s own
target/generated-sources/grpc at build time — generated fresh per module
from the one proto this module owns, never copy-pasted between services.
Two jobs the registry does: runtime vs. discovery
These two contract types are enforced differently once a service is running.
A runtime contract sits on the hot path. The Avro event
schemas are this project’s example: OrderEventProducer’s
emitter.send(event) only works at all because Apicurio’s Avro serializer
and deserializer agree on the schema, fetched from the registry at
publish/consume time; the event will not encode or decode
if the schema is wrong or missing. Because of that, Apicurio can enforce governance
on it: a compatibility rule configured
on the artifact can reject a schema change that would break existing
consumers, at publish time, before it ever reaches one.
A discovery contract is different. The .proto here is compiled ahead
of time into generated stubs, not fetched at request time — nothing fails
at runtime if the registry doesn’t have a copy of it. It still deserves a
single, versioned home (so a consumer can always find the exact shape a
gRPC service expects), but the coupling is loose and offline rather than
tight and online.
Figure 4.2 shows the distinction as two numbered paths through the same
registry. The runtime path is the four steps this project runs: ①
OrderEventProducer serializes OrderPlaced and registers (or fetches) its
schema against Apicurio, ② the Avro bytes plus a 4-byte schema id land on the
order-placed Kafka topic, ③ OrderPlacedConsumer in notification-service
fetches that same schema by id, and ④ it deserializes the bytes back into an
OrderPlaced record before persisting a Notification — the round
trip the previous chapter’s OrderEventProducer/OrderPlacedConsumer pair
walks through in code. The discovery path (⑤–⑧) goes beyond what is wired up: it shows all four services publishing
contracts — OpenAPI, Protobuf, GraphQL SDL, and Avro again — into the same
registry, with OpenMetadata ingesting them alongside the Postgres schemas and
Kafka topics to build a lineage graph. Of those four, only Protobuf is a checked-in discovery contract in this project (inventory.proto, packaged
into the contracts jar as covered above); order-service and
inventory-service don’t add the quarkus-smallrye-openapi extension, so
there’s no /openapi.json document generated today for REST to publish
anywhere, and while graphql-gateway’s quarkus-smallrye-graphql extension
does derive a GraphQL SDL schema from GatewayApi’s annotations (what the
diagram draws as schema.graphql), nothing in this repository registers it
anywhere or ingests it into a catalog. Figure 4.2 marks ⑤–⑧ as the target
shape, not code that runs today.
Conflating the two is how teams get surprised in both directions: treating
the Avro schema as documentation misses that a bad change breaks every consumer
immediately, and treating the proto as if it were enforced like the Avro schema
overstates what compiling against a checked-in .proto guarantees at runtime.
What’s registered, and how to see it
application.properties in each producing/consuming service configures the
Apicurio Avro serde explicitly (see the previous chapter’s note on why
value.serializer is pinned rather than autodetected), pointing at the
compose stack’s Apicurio instance. demo-kafka.sh is where this
project shows the registry doing work: it places an order (triggering
OrderEventProducer.publish), reads the raw bytes back off the Kafka topic with
kcat, bypassing any Avro deserializer, and
asserts the first byte of the record value is 0x00, the Apicurio/Confluent
wire-format magic byte that precedes a 4-byte schema id and the Avro-encoded
payload. It also asserts the value does not start with 0x7b ({), which
would mean the serializer had silently regressed to plain JSON. Then it
confirms Apicurio’s own artifact count went up after the publish, and that a
matching artifact exists under either the topic name or the record’s fully
qualified name capstone.order.v1.OrderPlaced — confirming from the
registry’s side that a schema was registered for this run, not just that bytes
arrived.
Raw-byte wire-format proof plus registry-side confirmation is cheap evidence that the
contract is enforced and not merely declared in a .avsc file and ignored at runtime.
Why a catalog is a requirement, not an add-on — and why it isn’t built yet
A registry tells you the shape of one contract at a time. It does not tell you which products exist across the whole mesh, who owns each one, who consumes whom, or where a given piece of data ultimately came from. That’s a catalog’s job — ingesting from the registry, the databases, and the event backbone to assemble a discoverable picture with lineage: the producer-and-consumer graph across domains, so a change’s downstream blast radius is something you can look up rather than reverse-engineer from code.
Figure 4.3 repeats Figure 4.1 with each contract marked by its job: a filled dot for the one runtime contract (Avro, enforced live against Apicurio) and hollow dots for the three discovery contracts (OpenAPI, Protobuf, GraphQL SDL). That marking is also why the OpenMetadata box sits across a gap: everything to its left is running code or a checked-in artifact in this repository, and everything inside it is not.
This project does not build that catalog yet: there is no
OpenMetadata deployment, no ingestion job, and no demo-discovery.sh or
demo-om-lineage.sh in this repository today. The sibling Python reference
architecture layers OpenMetadata on top of the same kind of registry this
chapter describes. The OpenMetadata ingestion and lineage work is planned
but not yet built here, so this section stays conceptual and cites no catalog demo.
It belongs in this chapter because of the argument: a mesh’s premise is that domains own their data independently and other domains consume it without a central team brokering access. That premise only holds if products are discoverable and their contracts are trustworthy. Without a registry (this project has one), contracts drift and consumers break silently. Without a catalog (it does not yet have one), nobody can find products or see lineage across the whole mesh, so in practice teams fall back to asking around, which reintroduces exactly the bottleneck a mesh exists to remove. Discovery infrastructure is a precondition for federated governance to be computational, checked at publish time and recorded automatically, rather than enforced through team coordination. This project demonstrates the registry half of that claim in running code; the catalog half is future work.
Build, run, observe
cd demos && ./demo-kafka.sh
The next chapter’s data-planes discussion returns to this demo for the event backbone.
Here, the two assertion steps that concern contracts are the raw first-byte check (0x00, not {)
right after the order is placed, and the Apicurio artifact-count delta and
artifact-id match that follow it. Together they show that this project’s Avro
contracts hold at runtime.
What you learned
contractsis the single source of truth for both contract types in this project — Avro.avscfor events, compiled ahead of time into generatedSpecificRecords, and a.protofor the one gRPC call, scanned out of the module’s jar by each consuming service’s own build.- Runtime contracts (Avro, fetched/enforced live against Apicurio) and discovery contracts (the proto, compiled ahead of time) have different coupling; conflating them leads to surprises about what a contract guarantees.
- A catalog is a mesh requirement in principle — without one, discovery degrades back into a central team being asked where things are — but this project has not built one yet.
The next question is how data moves across the mesh on top of them — the synchronous read layer and the asynchronous event backbone.
Verification status: verified. demo-kafka.sh’s raw-byte magic-byte assertion passed and OrderPlacedAvroWireIT is green. One item remains unexercised: an intentionally incompatible Avro schema change being rejected by Apicurio at publish time is still not covered by any demo.