A sample Certificate Consumer that bridges the new CloudEvents-based Company Certificate Management API (CX-0135) to a legacy Apache Kafka backend built against the previous CCM message format (v2.4.0). It demonstrates how an existing Kafka-based consumer backend can be adapted to the new API without being rewritten — the adapter is an anti-corruption layer that translates between the two formats.
It follows the conventions of the reference implementation Metaform/certo
(Java 25, Spring Boot 4, Gradle Kotlin DSL, Jackson 3, OkHttp). It implements only the consumer side;
provider-side functions (issuing, fulfilling, hosting certificates) are out of scope.
📖 Migration guide — the endpoint-, status-, and field-level mapping from the old CCM format (v2.4.0) to the new CloudEvents API. It uses this adapter (the anti-corruption layer) as its explanatory device: every translation the guide describes is one this app performs.
NEW CCM (CloudEvents / HTTP) OLD CCM (JSON over Kafka)
Certificate ┌────────────────────────────┐ ┌──────────────────────────┐
Provider ─────────► POST /certificate- │ │ │
(e.g. certo) (1) │ notifications │ (3) │ topic: │
│ │ CREATED ├────────► ccm.certificates.inbound ──► Legacy
▲ │ ▼ │ push │ │ backend
│ │ (2) GET /certificates/{id}│ │ │ (unchanged)
│ (6) POST │ (metadata + docs pull) │ │ │
│ /certificate-acceptance-notifications │ │ topic: │
└──────────────┤ ▲ │ (5) │ ccm.feedback.outbound │◄── feedback
│ └───────────────┼─────────┤ │
│ (4) translate feedback │ consume │ │
└────────────────────────────┘ └──────────────────────────┘
CCM Migration Adapter
Inbound — receive a certificate (new → old):
- The provider sends a
CertificateLifecycleStatusCloudEvent toPOST /certificate-notifications. - On
CREATED(orMODIFIED), the adapter retrieves the certificate metadata (GET /certificates/{id}, JSON with adocuments[]array) and then each document binary (GET /documents/{id}). - It translates the certificate into old-CCM push messages (each document inline as base64) and publishes them to
the
ccm.certificates.inboundKafka topic the legacy backend already consumes. The old format carries one document, so the adapter emits one push per document.
Outbound — provide feedback (old → new):
4. The legacy backend validates the certificate and publishes an old-CCM status (feedback) message
to ccm.feedback.outbound.
5. The adapter consumes it, translates it to a new CertificateAcceptanceStatus outcome, and …
6. … reports it to the provider via POST /certificate-acceptance-notifications.
The provider can also query the adapter's recorded decision via
GET /certificate-acceptance-status/{exchangeId}.
Status mapping
Old (feedback certificateStatus) |
New (AcceptanceStatus) |
|---|---|
RECEIVED |
RETRIEVED |
ACCEPTED |
ACCEPTED |
REJECTED |
REJECTED |
Old certificateErrors[] + nested locationErrors[] are flattened into one new errors[]; a
per-location error carries its BPN as the specifier.
Identity mapping
| Old | New |
|---|---|
documentId (feedback) |
certificateId |
| (correlation) | exchangeId (from a CREATED event) |
Field mapping (new → old push). The combined retrieval metadata is rich — issuer, validator,
registrationNumber, trustLevel, and structured enclosedSites are all returned — so the adapter maps them straight
into the old push. Only uploader has no source (removed in the new protocol) and is emitted as null.
Caveats.
- A certificate may have multiple documents; the old push carries one, so the adapter emits one push per document (per the migration guide; primary-only or a list are alternatives).
WITHDRAWNlifecycle events have no old-CCM equivalent and are logged only (not bridged).CertificateFulfillmentStatusevents are acknowledged but ignored — this adapter is push-only and opens no consumer-initiated requests.- Feedback can only be reported for a certificate the adapter saw a
CREATEDevent for (onlyCREATEDopens an exchange, per CX-0135 §2.2.4).
Prerequisites: Docker (for Kafka). The Gradle build provisions JDK 25 automatically.
- Start Kafka:
docker compose up -d
- (Optional) Run a
certoprovider onhttp://localhost:8080so retrieval and acceptance callbacks have a real counterpart. Otherwise the inbound flow logs a retrieval failure and the outbound flow logs a failed callback — the translation still runs. - Run the adapter (on port 8081):
./gradlew bootRun
Send a CREATED lifecycle event (the adapter will pull the certificate and publish a legacy push):
curl -i -X POST http://localhost:8081/certificate-notifications \
-H 'Content-Type: application/cloudevents+json' \
-d '{
"specversion": "1.0",
"type": "org.catena-x.ccm.CertificateLifecycleStatus.v1",
"source": "urn:bpn:BPNL0000000001AB",
"subject": "BPNL0000000002CD",
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"time": "2025-05-04T07:00:00Z",
"sourcebpn": "BPNL0000000001AB",
"data": {
"exchangeId": "exch-001",
"certificateId": "cert-001",
"version": 1,
"status": "CREATED",
"datasetId": "dataset-001",
"certificateType": "ISO9001",
"validFrom": "2023-01-25",
"validUntil": "2026-01-24",
"locationBpns": ["BPNS00000003AYRE"]
}
}'Observe the translated old-CCM push on Kafka:
docker exec ccm-migration-kafka /opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic ccm.certificates.inbound --from-beginningPublish a legacy feedback message (as the legacy backend would). documentId is the certificateId
from the inbound step:
docker exec -i ccm-migration-kafka /opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server localhost:9092 --topic ccm.feedback.outbound <<'EOF'
{"header":{"context":"CompanyCertificateManagement-CCMAPI-Status:1.0.0","version":"3.1.0"},"content":{"documentId":"cert-001","certificateStatus":"ACCEPTED","locationBpns":["BPNS00000003AYRE"]}}
EOFThe adapter reports a CertificateAcceptanceStatus CloudEvent to the provider and records the outcome:
curl -s http://localhost:8081/certificate-acceptance-status/exch-001 | jqsrc/main/resources/application.yaml (ccm.*):
| Key | Default | Meaning |
|---|---|---|
ccm.provider-base-url |
http://localhost:8080 |
Provider data plane (retrieve + acceptance) |
ccm.consumer.bpn/.source |
BPNL0000000002CD |
This adapter's consumer identity |
ccm.provider.bpn/.source |
BPNL0000000001AB |
The provider's identity |
ccm.kafka.certificates-topic |
ccm.certificates.inbound |
Topic the adapter publishes legacy push to |
ccm.kafka.feedback-topic |
ccm.feedback.outbound |
Topic the adapter consumes legacy feedback from |
spring.kafka.bootstrap-servers |
localhost:9092 |
Kafka broker |
common/cloudevent CloudEvents envelope, codec, dedup (mirrors certo)
common/model Lifecycle / Acceptance status models
common/web Error handling
config MigrationProperties
client Provider HTTP clients (metadata retrieve, document retrieve, acceptance callback)
legacy Old-CCM v2.4.0 message records + LegacyTranslator (the bridge core)
consumer Notification API + CertificateConsumerService + CorrelationStore
bridge Kafka producer (push out) + feedback listener (feedback in)