Carry a governed EBX change safely into Kafka

THE EVENT JOURNEY · ANIMATED SUMMARY
Download the working client
The companion client is available as a thin adapter JAR or as a bundled JAR containing the Apache Kafka runtime dependencies. Both downloads, the example properties file, SHA-256 checksums, source code, compatibility boundary, and deployment instructions are on the product page. The public source repository is KinshukON/ps-mdm-kafka-client.
What ps-mdm already gives you
The framework separates EBX-facing behavior from broker-specific behavior. MessagingConfiguration loads configuration and creates a MessagingClient. That facade transforms EBX Adaptation records into JSON, applies retry logic, and delegates transport calls to a vendor adapter implementing MessagingClientInterface. Kafka, RabbitMQ, or another transport can therefore sit behind the same EBX service.
| Verified ps-mdm class | Responsibility | Kafka role |
|---|---|---|
MessagingConfiguration | Loads the configuration class and client class by reflection; caches the facade | Creates the Kafka adapter with a MessagingConfiguration constructor argument |
PropertiesFileMessagingConfiguration | Reads host, port, credentials, destinations, timeout, retries, and formatting | Useful baseline, but extend it for native Kafka properties such as bootstrap servers, group ID, serializers, and security |
MessagingClient | Facade for publish, subscribe, queue operations, transformation, retry, and shutdown | Calls publishMessage(topic, message) and subscribe(topic, timeout) on the adapter |
MessagingClientInterface | Transport service-provider interface | The Kafka adapter implements topic operations; queue discovery operations can be explicitly unsupported |
MessageOutboundTransformationImpl | Serializes one EBX record | Creates the Kafka record value |
MessageInboundTransformationImpl | Parses a JSON object to Map<String,Object> | Turns consumed record values into an intermediate map |
PublishSelectedRecordsService | Publishes selected table rows from an EBX user service | One topic record per selected EBX record |
PSModuleRegistration | Invokes messaging shutdown before module unregister | Closes the long-lived Kafka producer/consumer resources |
The outbound message contract
The default outbound transformer wraps a record in two objects. messageMetadata identifies the EBX dataspace, dataset, and table; messagePayload contains the JSON representation produced by JSONUtil.jsonObjectForAdaptation(adaptation, true). A representative event is:
{
"messageMetadata": {
"messageDataspace": "Reference",
"messageDataset": "CustomerMaster",
"messageTable": "/root/Customer"
},
"messagePayload": {
"customerId": "C-1042",
"legalName": "Northstar Components",
"status": "ACTIVE"
}
}This is a useful transport envelope, but it is not yet a durable enterprise event contract. Add an event ID, event type, schema version, occurred-at time, source commit or changeset identifier, operation (CREATE, UPDATE, DELETE), record primary key, actor/service identity, and trace ID. Keep broker metadata such as partition and offset outside the business payload.
Configure the framework—and mind the exact keys
The framework can read the main EBX properties file or an alternate file named by the JVM system property ebx.messaging.properties. Java property names are case-sensitive. The reviewed code looks up ebx.messaging.configurationclassname in lowercase, even though the sample properties document shows ebx.messaging.configurationClassName. Use the runtime constant’s lowercase spelling unless you deliberately correct and test the framework.
ebx.messaging.enable=true
ebx.messaging.configurationclassname=com.acme.ebx.kafka.KafkaMessagingConfiguration
ebx.messaging.clientclassname=com.acme.ebx.kafka.KafkaClient
# Existing ps-mdm properties
ebx.messaging.host=kafka-1.internal.example
ebx.messaging.port=9093
ebx.messaging.inbound.destinationname=mdm.customer.command.v1
ebx.messaging.outbound.destinationname=mdm.customer.event.v1
ebx.messaging.connection.timeout=10000
ebx.messaging.connection.retry=3
ebx.messaging.connection.retry.delay=1000
ebx.messaging.connection.reuse=true
ebx.messaging.outbound.useprettyprint=false
# Extension properties read by KafkaMessagingConfiguration
ebx.messaging.kafka.bootstrap.servers=kafka-1.internal.example:9093,kafka-2.internal.example:9093
ebx.messaging.kafka.group.id=ebx-customer-command-v1
ebx.messaging.kafka.client.id=ebx-mdm
ebx.messaging.kafka.security.protocol=SASL_SSL
ebx.messaging.kafka.sasl.mechanism=SCRAM-SHA-512
ebx.messaging.kafka.acks=all
ebx.messaging.kafka.enable.idempotence=trueDo not keep production passwords directly in this file. The existing loader treats only ebx.messaging.password as secret for logging purposes; that is not a secret store. Resolve credentials through the application server or an approved secrets provider and keep JAAS strings, keystore passwords, and OAuth client secrets out of source control and diagnostic output.
Implement the missing Kafka adapter
The adapter must expose the exact constructor used by reflection: one argument of type MessagingConfiguration. Keep a thread-safe KafkaProducer<String,String> for outbound traffic. A KafkaConsumer is not thread-safe, so create and use it inside one controlled subscriber worker or guard it with strict single-thread ownership.
public final class KafkaClient extends AbstractMessagingClientInterface {
private final KafkaProducer<String, String> producer;
private final KafkaMessagingConfiguration config;
public KafkaClient(MessagingConfiguration base) {
super(base);
this.config = (KafkaMessagingConfiguration) base;
this.producer = new KafkaProducer<>(config.producerProperties());
}
@Override
public void publishMessage(String topic, OutboundMessage message) throws Exception {
String key = message.getHeaderProperty("recordKey");
ProducerRecord<String, String> record =
new ProducerRecord<>(topic, key, message.getBody());
record.headers().add("correlationId", bytes(message.getCorrelationID()));
producer.send(record).get(config.deliveryTimeoutMs(), TimeUnit.MILLISECONDS);
}
@Override
public List<InboundMessage> subscribe(String topic, int timeoutMs) {
try (KafkaConsumer<String, String> consumer =
new KafkaConsumer<>(config.consumerProperties())) {
consumer.subscribe(List.of(topic));
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(timeoutMs));
List<InboundMessage> result = new ArrayList<>();
for (ConsumerRecord<String, String> record : records) {
InboundMessage message = new InboundMessage();
message.setBody(record.value());
message.setMessageId(topic + "-" + record.partition() + "-" + record.offset());
result.add(message);
}
return result;
}
}
@Override public void shutdown() { producer.flush(); producer.close(); }
}The sketch intentionally blocks on the producer acknowledgement because the ps-mdm facade treats a returned call as success. In a higher-throughput service, collect asynchronous acknowledgements and expose failure deterministically; never log “sent” before Kafka has accepted the record under the selected acknowledgement policy. Implement the remaining interface methods with a documented behavior. For Kafka, queue-oriented methods can throw UnsupportedOperationException; topic listing can use AdminClient; createConnection and resetConnection should reflect Kafka client lifecycle rather than imitate a JMS connection.
Publish from EBX
PublishSelectedRecordsService already demonstrates the happy path: execute the selected-record request, create an OutboundMessage, set its delivery time and Adaptation, then call messageClient.publishMessage(null, message). Passing null chooses the configured outbound destination. The default transformer serializes the adaptation immediately before delegation.
MessagingClient client = MessagingConfiguration.getMessagingClient();
OutboundMessage event = new OutboundMessage();
event.setAdaptation(customerRecord);
event.setDeliveryTime(System.currentTimeMillis());
event.setCorrelationID(traceId);
event.addHeaderProperty("recordKey", customerId);
client.publishMessage("mdm.customer.event.v1", event);Use the stable master-record key as the Kafka key when per-entity ordering matters. Do not use a random key for updates to the same customer. The stock user service is explicitly example code: it catches all exceptions and prints a stack trace, which is insufficient for a governed production operation. Surface an EBX OperationException or durable publication status, authorize the service, audit the action, and avoid exposing restricted fields in the serialized adaptation.
Subscribe and apply changes in EBX
MessagingClient.subscribe(topic, timeout) delegates to the adapter and runs each body through MessageInboundTransformationImpl, returning a list of maps. That is only deserialization. It does not locate a dataset, validate a table, create a procedure, or commit a record. Your subscriber application layer must perform those EBX steps explicitly.
- Run a single controlled consumer worker per consumer instance—typically an application-server-managed scheduled task or service, not an unbounded thread created by a table trigger.
- Poll a bounded batch and validate envelope type, schema version, source, topic, and allowed EBX target.
- Resolve the target dataspace, dataset, and table from an allowlist. Never trust arbitrary paths from the message.
- Execute the write in an EBX
Procedurewith an authenticated service session. Apply table constraints, permissions, and business validation. - Store the Kafka event ID or topic-partition-offset in an EBX integration ledger in the same EBX transaction as the business write.
- Commit the Kafka offset only after the EBX transaction succeeds. On a transient failure, leave the offset uncommitted. On a permanent validation failure, publish a sanitized failure event or dead-letter record, then commit according to the documented policy.
This produces at-least-once delivery with idempotent application. Kafka’s idempotent producer prevents certain producer retry duplicates; it does not make a Kafka consume-and-EBX-write operation atomic. The integration ledger closes that gap by making duplicate delivery detectable inside EBX.
Transaction boundary: prefer an outbox for automatic publication
A synchronous call from an EBX table trigger to Kafka creates a dual-write problem: the EBX transaction may commit after Kafka fails, or Kafka may accept the event before EBX rolls back. For automatic publication, write an outbox row inside the same EBX procedure as the master-data change. A scheduled publisher reads pending outbox rows, publishes with a deterministic event ID, records the acknowledgement, and marks the row delivered. Consumers still remain idempotent.
Use the existing manual publish service for deliberate operator-driven exports or controlled testing. Use an outbox when the business contract says every committed change must eventually produce an event.
Source-level issues to address before production
- Inbound fallback: the reviewed
recieveMessagesmethod falls back togetOutboundDestination()when the inbound destination argument is empty. Correct it togetInboundDestination()and cover the behavior with a regression test. - Body-only publication:
PublishUtils.publishDataspaceDiffsets a string body but calls the default publish path, which attempts to transform a nullAdaptation. Add a pass-through transformer or a body-only overload before using this utility. - Retry meaning:
ebx.messaging.connection.retrycontrols total loop attempts in the reviewed code, despite being described as retries. Set and document it accordingly or correct the loop. - Failure reporting: the sample publish service swallows failures after
printStackTrace(). Replace that with structured logging, an EBX-visible failure, and operational metrics. - Configuration drift: sample documentation refers to older class names and a mixed-case property key. Treat compiled constants and tested runtime configuration as authoritative.
- Payload governance: the generic adaptation serializer may include fields that should not leave EBX. Use a purpose-built outbound transformation with an explicit allowlist and schema.
Production checklist
- Package
ps-mdm, the Kafka adapter JAR, and version-compatible Kafka client dependencies on the application-server classpath without duplicate client versions. - Confirm the EBX module registers the intended user service or scheduled subscriber and invokes shutdown on undeploy.
- Create topics with reviewed partition, replication, retention, compaction, and authorization settings.
- Use TLS and workload identities; test credential and certificate rotation.
- Validate schemas and compatibility before deploy; quarantine malformed or unsupported versions.
- Measure publish acknowledgements, error and retry rates, consumer lag, EBX apply latency, rejected records, dead-letter volume, and end-to-end freshness.
- Test broker loss, rebalance, duplicate delivery, poison records, EBX rollback, restart after publish-before-status-update, and replay from the oldest retained event.
- Reconcile Kafka events against the EBX integration ledger and business records.
What this design achieves
The ps-mdm framework is a useful anti-corruption layer: EBX services work with EBX records and framework messages, while a replaceable adapter owns Kafka APIs. With a Kafka adapter, explicit event contract, keyed ordering, an outbound outbox, an inbound idempotency ledger, secured configuration, and failure tests, it becomes a credible publish/subscribe bridge rather than a demo that merely sends JSON.
Use OffsetDock to inspect the topics, keys, payload envelopes, partitions, and consumer lag during development. Validate EBX APIs and deployment behavior against the exact supported release in the official TIBCO EBX documentation, and Kafka properties against the official Apache Kafka documentation.
Implementation map. The reviewed ps-mdm code is the EBX-facing framework, not the complete Kafka connector. Its value is the seam: EBX services and transformations stay transport-neutral while a separately packaged adapter implements Kafka producer, consumer, and administration behavior.
Verified ps-mdm architecture
| Layer | Verified source | What must be added | Runtime proof |
|---|---|---|---|
| EBX entry point | PublishSelectedRecordsService; a dataspace browse service; module shutdown hook | Activation permissions, scheduled subscriber, durable error reporting | Service registration, authorization, and module undeploy test |
| Framework | MessagingConfiguration, MessagingClient, transformations, message types | Correct inbound fallback, pass-through body publishing, governed schemas | Unit tests for reflection, reload, retries, and transformation |
| Kafka | Adapter class name appears in framework documentation; implementation is absent | Kafka adapter JAR and version-compatible Kafka client libraries | Producer acknowledgement, consumer rebalance, TLS/authentication tests |
| Consistency | No cross-system transaction coordinator is implemented | Outbound outbox and inbound idempotency ledger | Crash/replay drills and end-to-end reconciliation |
Outbound publication sequence
- EBX selects the record. The sample user service iterates the selected
Adaptationobjects and creates oneOutboundMessageper record. - The facade resolves configuration.
MessagingConfiguration.getMessagingClient()reads the properties, instantiates the configuration and adapter by reflection, and returns the cached facade. - The transformer builds the envelope.
MessageOutboundTransformationImpldelegates toTransformationUtils, which emits EBX location metadata and the record payload. - The adapter publishes. The missing Kafka implementation converts the framework message into a keyed
ProducerRecordand waits for the configured acknowledgement. - Operations retain evidence. Record event ID, key, topic, partition, acknowledgement latency, and outbox state without logging sensitive payloads.
Key by master identity
All changes for the same customer, product, or reference entity should normally use the same stable Kafka key so partition ordering has business meaning.
Publish from an outbox
Commit the master-data change and outbox row together, then publish asynchronously. A table trigger that calls Kafka directly cannot make both systems atomic.
Version the envelope
Make event type, schema version, operation, source, event ID, and occurrence time explicit. The generic record JSON is an implementation format, not a complete contract.
Inbound application sequence
- Poll under one owner. A managed worker owns the non-thread-safe Kafka consumer and polls a bounded batch.
- Validate before EBX lookup. Verify topic, source, type, schema version, signature where applicable, and payload limits.
- Resolve through an allowlist. Map the event type to a fixed EBX dataspace, dataset, table, and field mapping; never execute an arbitrary message-supplied path.
- Write inside an EBX procedure. Use a service session, invoke the supported repository APIs, and allow constraints and permissions to reject invalid changes.
- Deduplicate transactionally. Store the event ID or topic-partition-offset in an integration ledger in the same EBX transaction as the record change.
- Commit the Kafka offset last. Commit only after EBX succeeds. Classify permanent failures for quarantine; leave transient failures retryable.
Deployment shape
- Application server: deploy ps-mdm, the custom Kafka adapter, and exactly one compatible Kafka client dependency set in the intended classloader.
- Configuration: use the lowercase runtime key
ebx.messaging.configurationclassname; externalize secrets and native Kafka security settings. - EBX: register services and scheduled tasks explicitly, bind them to approved datasets/dataspaces, and verify permissions with a non-administrator service identity.
- Kafka: provision event, command, retry, and dead-letter topics only where the failure policy requires them; review partitions, replication, retention, and ACLs.
- Observability: correlate EBX transaction or outbox ID with Kafka event ID; measure acknowledgements, retries, lag, apply outcome, quarantine, and reconciliation gaps.
Primary references and next reading
Use version-matched documentation for configuration defaults. The links below point to maintained upstream documentation rather than copied defaults.
CHAPTER KNOWLEDGE CHECK · DRAG, DROP, OR TAP

Historical comments from Datanizant
No public comments on this article
No approved public comments were included in the WordPress export for this article.