> For the complete documentation index, see [llms.txt](https://host2host.onibonje.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://host2host.onibonje.com/docs/46-database-driven-events.md).

# Database-Driven Events

## 1. Overview

The event backbone is **database-driven** — not hardcoded Kafka topic names or partition keys in Java. Producers and consumers use **logical event codes** (`PAYMENT_POSTED`, `CONFIG_PUBLISHED`). Physical destinations (topic/queue names), broker selection, and **partition strategies** are resolved at runtime from the configuration database.

| Anti-pattern (forbidden)                               | Platform approach                                      |
| ------------------------------------------------------ | ------------------------------------------------------ |
| `kafka:h2h.payment.posted` hardcoded in routes         | `EventPublisher.publish("PAYMENT_POSTED", envelope)`   |
| `partition key = partnerId` baked into producer code   | `partition_key_fields` on `event_channel_def`          |
| Topic naming convention `h2h.{module}.{event}` in code | `destination_name` configured per scope in DB          |
| New event = code change + deploy                       | New row in `event_def` + `event_channel_def` → publish |

**Modules:** `h2h-event-extensions` (`EventPublisher`, `EventChannelResolver`, `EventConsumerRegistry`).

See also: [21 Event-Driven Runtime Extensibility](/docs/21-event-driven-runtime-extensibility.md), [04 Database-Driven Configuration](/docs/04-database-driven-configuration.md).

***

## 2. Architecture

```mermaid
flowchart TB
  subgraph producers [Producers - vendor neutral]
    Camel[h2h-camel-core]
    File[h2h-file-management]
    Admin[h2h-admin-api]
    Obs[h2h-observability]
  end

  subgraph resolver [Runtime - h2h-event-extensions]
    Pub[EventPublisher]
    ECR[EventChannelResolver]
    Cache[EventChannelCache]
    Part[PartitionKeyComputer]
  end

  subgraph config [Config DB]
    ED[event_def]
    ECD[event_channel_def]
  end

  subgraph broker [Message Broker]
    K[Kafka topics - names from DB]
    R[RabbitMQ queues - names from DB]
  end

  subgraph consumers [Consumers]
    Sub[EventExtensionDispatcher]
    WH[Webhook Dispatcher]
    Inv[Cache invalidation listeners]
  end

  Camel --> Pub
  File --> Pub
  Admin --> Pub
  Pub --> ECR
  ECR --> Cache
  Cache --> ED
  Cache --> ECD
  ECR --> Part
  Part --> K
  Part --> R
  K --> Sub
  K --> WH
  K --> Inv
```

**On publish:** `EventChannelResolver` selects the most specific `event_channel_def` for `(event_code, scope from H2hContext)` → `destination_name` + `partition_strategy` → broker send.

**On consume:** `EventConsumerRegistry` loads all enabled channels for subscribed `event_code` values; refreshes on config publish — **no topic names in `@KafkaListener` annotations**.

***

## 3. Configuration Tables

### 3.1 `event_def` — logical event catalog

| Column              | Description                                                                |
| ------------------- | -------------------------------------------------------------------------- |
| `event_code`        | Stable identifier — `PAYMENT_POSTED`, `FILE_DELIVERED`, `CONFIG_PUBLISHED` |
| `category`          | `TRANSACTION`, `FILE`, `CONFIG`, `JOB`, `AUDIT`, `ALERT`, `EXTENSION`      |
| `description`       | Admin UI label                                                             |
| `payload_schema`    | JSON Schema for envelope `payload` (optional)                              |
| `ordering_required` | Boolean — if true, partition strategy must not be `NONE`                   |
| `enabled`           | Boolean                                                                    |
| `system_event`      | Boolean — platform seed; custom events = false                             |

**Rule:** Application code references **`event_code` only** — never physical topic strings.

### 3.2 `event_channel_def` — physical destination + partitioning

| Column                 | Description                                                              |
| ---------------------- | ------------------------------------------------------------------------ |
| `channel_id`           | UUID                                                                     |
| `event_code`           | FK → `event_def`                                                         |
| `scope_type`           | `GLOBAL`, `ORG`, `REGION`, `COUNTRY`, `PARTNER`, `MODULE`, `SERVICE`     |
| `scope_id`             | e.g. `NG`, `WEST_AFRICA`, `ACME_CORP`, `h2h-payments-routes`             |
| `environment`          | `sandbox`, `production`, or `*`                                          |
| `broker`               | `KAFKA`, `RABBITMQ`                                                      |
| `destination_type`     | `TOPIC`, `QUEUE`, `DELAYED_EXCHANGE`                                     |
| `destination_name`     | Physical name — e.g. `heirs.ng.payments.posted`, `h2h-jobs-execute-west` |
| `partition_strategy`   | See §4                                                                   |
| `partition_key_fields` | JSON array — envelope/header field names                                 |
| `partition_count`      | Expected partitions (provisioning hint / validation)                     |
| `retention_ms`         | Optional — topic retention                                               |
| `compacted`            | Boolean — log compaction                                                 |
| `priority`             | Tie-breaker when multiple channels match same specificity                |
| `enabled`              | Boolean                                                                  |
| `status`               | `DRAFT`, `PUBLISHED`, `SUPERSEDED`                                       |

**Scope resolution (most specific wins):**

```
SERVICE + scope_id  >  MODULE  >  PARTNER  >  COUNTRY  >  REGION  >  ORG  >  GLOBAL
```

Same pattern as integration profiles and customization profiles.

### 3.3 `event_subscription` — handlers (updated)

| Column              | Description                                       |
| ------------------- | ------------------------------------------------- |
| `subscription_id`   | UUID                                              |
| `event_code`        | FK → `event_def` (not topic pattern)              |
| `scope`             | `GLOBAL`, `COUNTRY`, `PARTNER`                    |
| `scope_id`          | Optional                                          |
| `handler_type`      | `SCRIPT`, `CAMEL_ROUTE`, `WEBHOOK`, `JOB_TRIGGER` |
| `handler_ref`       | script\_id, route URI, webhook\_id, job\_id       |
| `filter_expression` | JSONata on event payload                          |
| `consumer_group`    | Optional — override default `h2h-{service}`       |
| `enabled`           | Boolean                                           |

### 3.4 `webhook_subscription` — updated

| Column              | Description      |
| ------------------- | ---------------- |
| `event_code`        | FK → `event_def` |
| `target_url`        | HTTPS endpoint   |
| `auth_ref`          | Vault credential |
| `filter_expression` | JSONata          |
| `retry_policy`      | JSON             |

***

## 4. Partition Strategies

Partitioning is **configuration**, not code. `PartitionKeyComputer` reads `partition_strategy` + `partition_key_fields` from the resolved channel.

| Strategy         | When to use                              | `partition_key_fields` example       | Effect                                  |
| ---------------- | ---------------------------------------- | ------------------------------------ | --------------------------------------- |
| `NONE`           | Fire-and-forget, no ordering (audit tap) | `[]`                                 | Round-robin / broker default            |
| `FIXED_FIELD`    | Single-dimension ordering                | `["partnerId"]`                      | All events for partner → same partition |
| `FIXED_FIELD`    | Country isolation                        | `["countryCode"]`                    | Per-country ordering                    |
| `FIXED_FIELD`    | Regional deployment                      | `["regionId"]`                       | Regional consumer affinity              |
| `COMPOSITE_HASH` | High volume + fair spread                | `["countryCode", "partnerId"]`       | Stable hash across partitions           |
| `COMPOSITE_HASH` | Module/service split                     | `["module", "service", "partnerId"]` | Colocate related traffic                |
| `CORRELATION_ID` | Strict per-transaction ordering          | `["correlationId"]`                  | Single payment lifecycle ordered        |

### 4.1 Recommended defaults (seed data — overridable in admin)

| `event_code`       | Default strategy | Default fields               | Rationale                         |
| ------------------ | ---------------- | ---------------------------- | --------------------------------- |
| `PAYMENT_POSTED`   | `FIXED_FIELD`    | `partnerId`                  | Partner ERP notifications ordered |
| `PAYMENT_FAILED`   | `FIXED_FIELD`    | `partnerId`                  | Same                              |
| `FILE_DELIVERED`   | `COMPOSITE_HASH` | `countryCode`, `partnerId`   | Volume + locality                 |
| `CONFIG_PUBLISHED` | `NONE`           | `[]`                         | Broadcast cache invalidation      |
| `JOB_EXECUTE`      | `FIXED_FIELD`    | `countryCode`                | Per-country job ordering          |
| `AUDIT_EVENT`      | `COMPOSITE_HASH` | `partnerId`, `correlationId` | Spread audit load                 |

### 4.2 Multi-region / multi-module example

| Scope                           | `event_code`     | `destination_name`           | `partition_strategy` | Fields                       |
| ------------------------------- | ---------------- | ---------------------------- | -------------------- | ---------------------------- |
| `GLOBAL`                        | `PAYMENT_POSTED` | `heirs.payments.posted`      | `FIXED_FIELD`        | `partnerId`                  |
| `REGION` / `WEST_AFRICA`        | `PAYMENT_POSTED` | `heirs.wa.payments.posted`   | `FIXED_FIELD`        | `countryCode`                |
| `COUNTRY` / `NG`                | `PAYMENT_POSTED` | `heirs.ng.payments.posted`   | `COMPOSITE_HASH`     | `partnerId`, `correlationId` |
| `MODULE` / `h2h-reconciliation` | `PAYMENT_POSTED` | `heirs.recon.payment.posted` | `FIXED_FIELD`        | `partnerId`                  |

Consumers in the reconciliation service subscribe to `PAYMENT_POSTED` — resolver binds them to `heirs.recon.payment.posted` when `MODULE` scope matches.

***

## 5. Producer API

```java
// h2h-event-extensions — no topic names
public interface EventPublisher {
    void publish(String eventCode, H2hEvent envelope, H2hContext context);
    void publish(String eventCode, H2hEvent envelope, EventPublishOverrides overrides);
}

public record EventPublishOverrides(
    String scopeType,   // optional — force REGION/COUNTRY channel
    String scopeId,
    Map<String, String> partitionFieldOverrides
) {}
```

**Resolution flow:**

1. Load `event_def` — validate `event_code` enabled.
2. `EventChannelResolver.resolve(eventCode, context)` → `event_channel_def`.
3. `PartitionKeyComputer.compute(channel, envelope)` → Kafka key / Rabbit routing key.
4. Send to `channel.destinationName()` on `channel.broker()`.

```java
// Inside h2h-camel-core — after Finacle post
eventPublisher.publish("PAYMENT_POSTED", H2hEvent.builder()
    .eventType("PAYMENT_POSTED")
    .correlationId(ctx.correlationId())
    .partnerId(ctx.partnerId())
    .countryCode(ctx.countryCode())
    .payload(paymentResult)
    .build(), ctx);
```

***

## 6. Consumer API

```java
public interface EventConsumerRegistry {
    void register(String eventCode, EventHandler handler);
    void refreshChannels();  // on CONFIG_PUBLISHED
}
```

* At startup: load all `PUBLISHED` `event_channel_def` rows → create dynamic Kafka consumers / Camel `kafka:` endpoints per destination.
* `EventExtensionDispatcher` matches consumed events to `event_subscription` by **`event_code`** from envelope `eventType`.
* Consumer group: `consumer_group` on subscription, else `h2h.{serviceName}` from runtime config.

**No `@KafkaListener(topics = "h2h.payment.posted")`** in application code.

***

## 7. Platform Seed vs Custom Events

| Type              | How created                                                              | Example                              |
| ----------------- | ------------------------------------------------------------------------ | ------------------------------------ |
| **System events** | Flyway seed in `event_def` + default `event_channel_def`                 | `PAYMENT_POSTED`, `CONFIG_PUBLISHED` |
| **Custom events** | Admin UI → `event_def` + channels → publish                              | `REGULATORY_EXPORT_NG_COMPLETE`      |
| **Plugin events** | L4 JAR registers `EventTypeProvider` SPI → merges into catalog on deploy | `TREASURY_FX_RATE_UPDATED`           |

Custom events require **`event_channel_def`** before publish — validator blocks orphan `event_code` references.

***

## 8. Cache Invalidation

| Event          | Channel resolution                                                |
| -------------- | ----------------------------------------------------------------- |
| Config publish | `EventChannelCache` evicts keys for affected `event_code` + scope |
| Mechanism      | Subscribe to `CONFIG_PUBLISHED` (resolved like any event)         |

***

## 9. Admin Portal

| Screen                    | Capability                                                                            |
| ------------------------- | ------------------------------------------------------------------------------------- |
| **Event catalog**         | CRUD `event_def` (custom events)                                                      |
| **Channel manager**       | Bind event → topic/queue, partition strategy, scope                                   |
| **Partition preview**     | Sample envelope → show computed partition key                                         |
| **Subscription designer** | `event_code` + handler (unchanged UX, logical codes)                                  |
| **Topic provisioner**     | Optional integration — create Kafka topic from `destination_name` + `partition_count` |

***

## 10. Infrastructure Provisioning

Topic **names** come from DB; DevOps may still provision clusters via Terraform/Strimzi using **exported channel definitions** from admin API:

```
GET /admin/api/v1/event-channels?environment=production&status=PUBLISHED
→ [{ destinationName, partitionCount, retentionMs, compacted }]
```

Helm values set **broker connection only** — not topic name lists.

***

## 11. Design Rules

| Rule                                                  | Rationale                       |
| ----------------------------------------------------- | ------------------------------- |
| No topic/queue strings outside `h2h-event-extensions` | Single resolver                 |
| No partition key logic in route modules               | DB-driven `partition_strategy`  |
| `event_code` is the only public contract              | Stable across broker migrations |
| Physical names may differ per country/region          | Regulatory / ops isolation      |
| `ordering_required=true` forbids `NONE` strategy      | Validator at publish            |
| ArchUnit: no `kafka:h2h.` in route modules            | Enforce indirection             |

***

## 12. Migration from Naming Conventions

Legacy docs referenced `h2h.payment.posted` — these become **default seed** `destination_name` values, not code constants:

| Legacy topic (seed default) | `event_code`       |
| --------------------------- | ------------------ |
| `h2h.payment.posted`        | `PAYMENT_POSTED`   |
| `h2h.payment.failed`        | `PAYMENT_FAILED`   |
| `h2h.file.delivered`        | `FILE_DELIVERED`   |
| `h2h.config.published`      | `CONFIG_PUBLISHED` |
| `h2h.jobs.execute`          | `JOB_EXECUTE`      |
| `h2h.audit.events`          | `AUDIT_EVENT`      |

Banks may rename topics in `event_channel_def` without redeploying application JARs.

***

## 13. Related Documents

* [21 Event-Driven Runtime Extensibility](/docs/21-event-driven-runtime-extensibility.md)
* [30 Database Schema Reference](/docs/30-database-schema-reference.md)
* [14 Extensibility Framework](/docs/14-extensibility-framework.md) §9
* [04 Database-Driven Configuration](/docs/04-database-driven-configuration.md)
* [07 Multi-Country Deployment](/docs/07-multi-country-deployment.md)
* [38 Deployment Topology](/docs/38-deployment-topology.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://host2host.onibonje.com/docs/46-database-driven-events.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
