Skip to content

Use Case Document

System: Integration Platform (IIP) Related docs: Architecture · Data Model · Implementation Plan · Generalization Strategy

Naming note: the platform keeps its iip- prefix and its intern use case, but it is no longer intern-specific — interns is contract #1, not the schema. Use cases that read as intern-specific below are worked examples of a generic capability, and say so explicitly where that matters. See 06 — Generalization Strategy.

Use cases here cover the full platform, including everything staged for later releases (contract definition, adapter attachment, lifecycle events, DLQ replay, dashboards). Each use case is tagged with the release it belongs to so the backlog in the Implementation Plan traces directly back to a use case. Release numbers follow the re-sequenced roadmap (Implementation Plan §2).


1. Actors

ActorKindDescription
HR StaffHuman, primaryEnters and reviews intern records via the Web UI. Generically: a Record Submitter — any user submitting records against a contract; HR Staff is that role for the interns contract.
Integration DesignerHuman, primaryDefines a contract (fields, types, natural key, record types) and wires its targets by attaching adapter types — via the Control-Plane UI, without a developer or a redeploy. The actor the generalization work exists to serve.
Platform OperatorHuman, primaryOperates and maintains the middleware: monitors health, manages the DLQ, provisions instances.
Downstream Target SystemSystem, secondaryPostgreSQL, the payroll CSV feed, and any future target — receives fan-out from adapters.
Kafka BrokerSystem, supportingDurable transport; not a "user" but modeled where its behavior (ordering, redelivery) drives a use case.
New Adapter DeveloperHuman, secondaryEngineer extending the platform with a new adapter type for the catalog (postgres, csv, webhook, …). Note the boundary set by AD-10: developers add types; Integration Designers instantiate them as config.

This is a solo project, so Adapter Developer, Integration Designer, Platform Operator, and (for demo purposes) HR Staff are all hats worn by the same one person — the actors are separated here by role, not by headcount.


2. Use Case Diagram

The two dotted edges out of UC-13/UC-14 are the shape of the whole generalization: UC-1 can only happen for a contract that UC-13 created, and fans out only to what UC-14 attached. Before this work, both of those were compile-time facts; now they're runtime data.


3. Use Case Summary

IDNamePrimary ActorRelease
UC-1Submit Record to a ContractHR Staff / Record Submitter1 (interns) / 4 (any contract)
UC-2View RecordsHR Staff / Record Submitter1
UC-3Edit RecordHR Staff / Record Submitter7
UC-4Delete RecordHR Staff / Record Submitter7
UC-5Fan-out to Database TargetSystem (postgres adapter)1 (typed) / 5 (config-driven)
UC-6Fan-out to File TargetSystem (csv adapter)1 (typed) / 6 (config-driven)
UC-7Recover from Transient Target FailureSystem1
UC-8Quarantine Poison MessageSystem1
UC-9Add a New Adapter TypeAdapter Developer6
UC-10Monitor System Health & Consumer LagPlatform Operator1 (basic) / 8 (dashboards)
UC-11Review and Replay DLQ MessagesPlatform Operator8
UC-12Configure Active TargetsPlatform Operator6
UC-13Define a Contract via UIIntegration Designer6
UC-14Attach an Adapter via UIIntegration Designer6
UC-15Provision a Contract InstancePlatform Operator9 (Path A only, gated)

4. Detailed Use Cases

UC-1 — Submit Record to a Contract

Generic capability, intern worked example. This use case was originally written as "Submit Intern Record." It is now the generic intake path — submit a record conforming to contract X — and the intern walkthrough below is kept verbatim as the worked example, because it is contract #1 and the one that's actually built. Substitute forms for interns and nothing in the flow changes.

FieldDetail
ActorHR Staff (generically: Record Submitter)
GoalGet a new record reliably distributed to every target attached to its contract, with a single submission.
PreconditionsUI is reachable; Source Service is running; the contract exists in the Contract Registry (UC-13); its topics exist.
TriggerSubmitter fills out and submits the contract's form.

Main flow:

  1. HR Staff enters intern details in the UI and submits.
  2. UI performs client-side validation and sends POST /contracts/interns/records (Release 1–3: POST /interns).
  3. Source Service loads the interns contract (cached) and validates the payload against it — field names, types, required-ness, enum domains — rather than against a compiled DTO (Release 4+; Releases 1–3 use Bean Validation on a compiled CreateInternRequest).
  4. Source Service generates recordId (UUID) and occurredAt, and derives naturalKey per the contract's key strategy (for interns: the internId field).
  5. Source Service builds the canonical envelope wrapping the payload and validates the envelope against the Schema Registry (Release 4+ — Releases 1–2 build the flat canonical record directly via CanonicalMapper, with no registry step).
  6. Source Service publishes to interns.created, keyed by naturalKey.
  7. Source Service returns 202 Accepted with the recordId.
  8. UI displays a submission confirmation to HR Staff.

Alternate flows:

  • 3a. Payload fails contract validation: Source Service returns 400 Bad Request with field-level errors derived from the contract; UI displays them inline; no event is published. (This is the load-bearing alternate flow post-generalization: it's what stops a generic JSONB landing table from quietly absorbing garbage — see Implementation Plan §7.)
  • 3b. contractId doesn't exist: 404 Not Found; nothing published.
  • 5a. Envelope fails schema validation: treated as a server-side bug (should be unreachable if the envelope builder is correct); logged as a critical error; 500 returned; no event published. (Defensive path — the builder is the thing under test, not the schema.)
  • 6a. Kafka unavailable: publish fails; Source Service returns 503 Service Unavailable; UI shows a retry-able error. No partial state — the record is either fully accepted (published) or fully rejected.

Postconditions: Exactly one canonical event exists on the contract's created topic for this submission, or none exists at all (never a partial publish).

Related NFRs: at-least-once delivery, ordering by naturalKey, contract enforcement, runtime extensibility.


UC-2 — View Records

FieldDetail
ActorHR Staff (generically: Record Submitter)
GoalSee previously submitted records for a contract and their submission status.
PreconditionsAt least the Source Service's read path is available.

Main flow:

  1. HR Staff navigates to the records view.
  2. UI calls GET /contracts/interns/records (Release 1–3: GET /interns).
  3. Source Service returns the list of submitted records (from its own read model / query store — not by reading adapter targets, preserving loose coupling).
  4. UI renders the list, with columns driven by the contract's field definitions rather than a hardcoded table layout (Release 6).

Postconditions: none (read-only). Related NFRs: loose coupling — the UI never queries PostgreSQL or the CSV file directly.


UC-3 — Edit Record (Release 7)

FieldDetail
ActorHR Staff (generically: Record Submitter)
GoalCorrect or update details of an already-submitted record.
PreconditionsThe record already exists (has a prior created event); the contract declares an update-style record type.

Main flow:

  1. HR Staff opens an existing record in the UI and edits fields.
  2. UI sends PUT /contracts/interns/records/{naturalKey}.
  3. Source Service validates against the contract and builds an update-style envelope (new recordId, same naturalKey, occurredAt refreshed).
  4. Source Service publishes to interns.updated, keyed by naturalKey (same key as the original create, guaranteeing ordering).
  5. postgres adapter upserts on the contract's declared natural keyON CONFLICT (contract_id, natural_key) DO UPDATE on the generic records table, or the shaped table's own key column in shaped mode. (This is the one guarantee that genuinely generalized: ON CONFLICT (intern_id) DO UPDATE was always "upsert on the declared natural key," it just had only one declaration.)
  6. csv adapter rebuilds the CSV snapshot for that key (see Data Model §4) rather than appending.

Alternate flows:

  • 1a. Record doesn't exist: UI/Source Service returns 404.
  • 3a. The contract declares no update-style record type: 409 Conflict — an append-only contract cannot be edited.

Postconditions: Downstream targets reflect the updated values; the CSV file has no duplicate/stale line for this record.

Related NFRs: ordering-by-key (update must be processed after the create it modifies), idempotency (a redelivered update must not double-apply).


UC-4 — Delete Record (Release 7)

FieldDetail
ActorHR Staff (generically: Record Submitter)
GoalRemove a record from all downstream targets (e.g., offboarding an intern).

Main flow:

  1. HR Staff triggers delete in the UI.
  2. UI sends DELETE /contracts/interns/records/{naturalKey}.
  3. Source Service publishes a tombstone envelope to interns.deleted, keyed by naturalKey. The payload is minimal by design — a tombstone needs the key, not the data.
  4. postgres adapter deletes (or soft-deletes, per retention policy) the row matching (contract_id, natural_key).
  5. csv adapter rebuilds the CSV snapshot, omitting the record.
  6. Every step logged with recordId/contractId/traceId for audit purposes.

Postconditions: The record no longer appears in any downstream target; an audit trail of the deletion exists in logs (and optionally an audit table).

Related NFRs: audit logging, ordering-by-key (a delete must never be processed before its create).


UC-5 — Fan-out to Database Target

FieldDetail
Actorpostgres adapter (system)
GoalPersist every canonical event to PostgreSQL exactly-once in effect, despite at-least-once delivery — for any contract attached to this adapter.
TriggerNew message available on a subscribed topic ({contract}.created, or .updated/.deleted in Release 7) for the db-adapter consumer group.

Main flow:

  1. Adapter consumes the message.
  2. Adapter deserializes the message (Release 1: a local DTO matching the canonical JSON shape, not a shared Java type with Source Service; Schema Registry validation of the envelope is Release 4+).
  3. Adapter checks whether the envelope's contractId is attached to it and resolves that attachment's target mapping from the registry (Release 5+; before that, the mapping is compiled in and only interns exists).
  4. Adapter maps the envelope + payload to a SQL row — the generic records row by default, or a shaped table per the attachment's config.
  5. Adapter executes INSERT ... ON CONFLICT (record_id) DO NOTHING (or ON CONFLICT (contract_id, natural_key) DO UPDATE for update-style events).
  6. Adapter commits the Kafka offset.

Alternate flows:

  • 3a. contractId is not attached to this adapter: adapter skips and commits the offset. Not a failure — it's how one shared adapter serves a subset of contracts.
  • See UC-7 (transient failure) and UC-8 (poison message).

Postconditions: Exactly one row (or one correctly-updated row) exists per (contractId, naturalKey), regardless of redelivery count. Note that step 5's idempotency guard is unchanged by generalizationrecordId is an envelope field, so exactly-once-in-effect holds for any schema for free (06 §2.2).


UC-6 — Fan-out to File Target

FieldDetail
Actorcsv adapter (system)
GoalAppend every canonical event to the contract's CSV file exactly-once in effect.

Main flow:

  1. Adapter consumes the message.
  2. Adapter confirms the contractId is attached to it and resolves the attachment's file path + column mapping (Release 6+; before that, interns.csv is compiled in).
  3. Adapter checks the dedup store: has this recordId already been written?
  4. If not: adapter maps the envelope + payload to a CSV line and appends it, then records the recordId in the dedup store.
  5. Adapter commits the Kafka offset.

Alternate flows:

  • 3a. recordId already processed: adapter skips the write (no-op) and commits the offset directly — this is the idempotency guarantee, not a failure path.
  • See UC-7 and UC-8 for target/processing failures.

Postconditions: Exactly one CSV line exists per recordId, regardless of redelivery count. No two adapter instances write concurrently (single-writer constraint, see Architecture AD-6) — a constraint that now applies per file, so two contracts writing two different files may safely share one adapter instance.


UC-7 — Recover from Transient Target Failure

FieldDetail
ActorAny adapter (system)
GoalGuarantee no data loss when a target is temporarily unreachable.
TriggerA write to the target throws a classified-retriable error (timeout, connection refused, etc.).

Main flow:

  1. Adapter attempts to write to the target; write fails.
  2. Adapter classifies the failure as retriable.
  3. Adapter does not commit the Kafka offset.
  4. Adapter retries with bounded backoff (e.g., up to 3 attempts).
  5. If a retry succeeds, adapter commits the offset and processing resumes normally.
  6. If all bounded retries fail, the adapter stops consuming that partition and re-attempts on the next poll cycle (the message is never lost — it simply isn't committed) — implementation may either block-and-retry indefinitely at the poll level for infrastructure-down scenarios, or escalate to DLQ after a much larger ceiling, per adapter configuration.

Postconditions: No message is lost; other adapters' consumer groups are unaffected (failure isolation).

Related NFRs: failure isolation, at-least-once delivery.


UC-8 — Quarantine Poison Message

FieldDetail
ActorAny adapter (system)
GoalPrevent one unprocessable message from blocking every message behind it.
TriggerA message fails processing and is classified non-retriable, or a retriable failure exhausts its bounded retry count.

Main flow:

  1. Adapter attempts to process the message; it fails.
  2. Adapter classifies the failure as non-retriable (e.g., schema violation, unmappable data) — or retries are exhausted.
  3. Adapter publishes the original message plus error metadata (error type, stack summary, timestamp, adapter name, contractId) to iip.dlq.
  4. Adapter commits the offset on the source topic, unblocking the partition.

Postconditions: The pipeline keeps moving; the bad record is preserved (not lost) in iip.dlq for operator review (UC-11). Because quarantine is per-message, a contract with systematically bad payloads fills the DLQ with its own records without slowing any other contract on the same adapter.

Related NFRs: failure isolation, no silent data loss, pipeline liveness.


UC-9 — Add a New Adapter Type (Release 6)

FieldDetail
ActorAdapter Developer
GoalExtend the platform's catalog with a new kind of target, with zero changes to the UI, Source Service, or existing adapters.

Scope boundary (AD-10): this use case adds an adapter type to the catalog — a developer task, requiring code. Instantiating a type against a contract is UC-14 and requires no developer. The distinction matters: a UI that authored novel adapter logic would be a low-code product (Retool/n8n), an order of magnitude more surface than this platform, and is explicitly out of scope.

Main flow:

  1. Developer implements a new consumer service following the generic adapter pattern (Architecture §6): consume → deserialize/validate → contract filter → resolve mapping → idempotency gate → transform → write → classify-failure → retry/DLQ.
  2. Developer assigns the adapter its own consumer group.
  3. Developer registers the new type in the adapter catalog, declaring the config schema an attachment must supply (e.g. webhook needs endpoint + auth).
  4. Developer deploys the new adapter as an independent service.
  5. Existing services (UI, Source Service, other adapters) require no changes or redeploys.
  6. The new type is now selectable by an Integration Designer in UC-14 — for every contract, existing ones included.

Postconditions: The new type exists in the catalog and receives every event for contracts attached to it, going forward (and, if desired, replayed from topic retention/DLQ for backfill).

Related NFRs: extensibility-by-addition, independent deployability, runtime extensibility.


UC-10 — Monitor System Health & Consumer Lag

FieldDetail
ActorPlatform Operator
GoalKnow at a glance whether the platform is healthy and whether any consumer is falling behind.

Main flow (Release 1 — baseline):

  1. Operator queries each service's Actuator /health and /metrics.
  2. Operator opens Kafka UI to inspect consumer group lag per adapter.

Main flow (Release 8 — dashboards):

  1. Operator opens the Grafana dashboard.
  2. Dashboard shows lag per consumer group, DLQ depth, throughput, and per-adapter success/failure counts, sourced from Prometheus scraping each service's Actuator metrics endpoint.
  3. Panels can be broken down by contractId, so "which schema is generating the backlog" is answerable without adding a dashboard per schema.

Postconditions: Operator can identify a stuck or lagging adapter before it becomes a user-visible incident.


UC-11 — Review and Replay DLQ Messages (Release 8)

FieldDetail
ActorPlatform Operator
GoalInspect quarantined messages, fix the root cause, and safely reprocess them.

Main flow:

  1. Operator opens the admin dashboard's DLQ view.
  2. Dashboard lists DLQ messages grouped by contract / error type / adapter, non-destructively read from iip.dlq.
  3. Operator diagnoses and fixes the root cause (code fix, data correction, or a contract correction via UC-13).
  4. Operator selects one or more messages and triggers replay.
  5. Replay tool re-publishes the original envelope to its own contract's source topic ({contractId}.created/.updated/.deleted as appropriate) — the destination is read off the envelope, not configured per replay.
  6. Replay tool marks the DLQ entry as replayed (audit trail — not deleted, to preserve history).
  7. Normal fan-out and idempotency guarantees apply to the replayed message exactly as to any other.

Postconditions: Previously-quarantined data successfully reaches its targets; an audit trail records what was replayed, when, and by whom.


UC-12 — Configure Active Targets (Release 6)

FieldDetail
ActorPlatform Operator
GoalEnable/disable which adapter attachments are active without redeploying the core platform.

Main flow:

  1. Operator toggles an adapter attachment's enabled flag in the Contract Registry (via the control-plane UI or API).
  2. Adapters read their attachments from the registry at startup and on a refresh interval.
  3. A disabled attachment is skipped: the adapter still consumes and commits, but writes nothing for that contract — messages remain replayable from topic retention, and other contracts on the same adapter are untouched.

Postconditions: The set of active fan-outs is controlled by registry data, not by code changes or redeployments of the core platform.

What changed with generalization: enablement moved from per-adapter to per-attachment — i.e. per (contract, target) pair. "Turn off the CSV feed" is now answerable as "for which contract?", which it had to become the moment one adapter served more than one schema.


UC-13 — Define a Contract via UI (Release 6)

FieldDetail
ActorIntegration Designer
GoalRegister a new schema (fields, types, natural key, record types) so the platform can accept and route its records with no redeploy.
PreconditionsControl-Plane API and Contract Registry are running.
TriggerA new kind of record needs onboarding (e.g. forms).

Main flow:

  1. Designer fills the contract form in the UI: contractId, title, field definitions (name, type, required, queryable), natural-key strategy, and the record types the contract declares.
  2. UI validates field/key definitions client-side (e.g. the key strategy must reference declared fields).
  3. UI sends POST /contracts.
  4. Control-plane persists the definition to the Contract Registry and provisions the contract's topics.
  5. The parameterized source-service picks up the new contract (on refresh or next boot).
  6. The contract is now live for submissions (UC-1).

Alternate flows:

  • 3a. contractId already exists: 409 Conflict. Editing an existing contract is a versioned, compatibility-checked update (see Data Model §5), not an overwrite.
  • 4a. The definition fails server-side validation (unknown type, key referencing a missing field, duplicate field names): 400 with the offending paths; nothing persisted, no topics created.

Postconditions: A new contractId exists; records conforming to it are accepted and validated. No service was rebuilt or redeployed to make that true.

Related NFRs: runtime extensibility, no redeploy, contract enforcement.


UC-14 — Attach an Adapter via UI (Release 6)

FieldDetail
ActorIntegration Designer
GoalFan a contract's records out to a target by instantiating a catalog adapter type as config — not by writing one.
PreconditionsThe contract exists (UC-13); the desired adapter type exists in the catalog (UC-9).

Main flow:

  1. Designer picks an adapter type (postgres / csv / webhook) for a contract.
  2. Designer enters that type's required target config — table + write mode, file path + columns, or endpoint + auth.
  3. UI sends POST /contracts/{id}/adapters.
  4. Registry stores the attachment.
  5. The relevant adapter, filtering by contractId, begins writing on its next refresh. Existing adapters and contracts are unaffected — the isolation principle from UC-9 applies unchanged.

Alternate flows:

  • 2a. Config fails the type's declared config schema: 400; nothing attached.
  • 5a. The target is unreachable when the first record arrives: ordinary UC-7 territory — retry, then DLQ. A bad attachment degrades into a quarantine, not a data loss.

Postconditions: The target receives the contract's records going forward.

Related NFRs: extensibility-by-addition, independent deployability, runtime extensibility.


UC-15 — Provision a Contract Instance (Release 9 — Path A only, gated)

FieldDetail
ActorPlatform Operator
GoalRun a schema in its own isolated pod set, rather than sharing the platform's services.
PreconditionsKubernetes + the IIP operator are deployed; and hard isolation has become a real, stated requirement.

Main flow:

  1. Operator (or the control-plane UI) writes an IIPInstance custom resource naming the contract.
  2. The operator reconciles it into: the contract's topics, a configured source-service pod, and attached adapter pods drawn from the same catalog images used in Path B.
  3. The instance serves that contract alone, in its own namespace.

Postconditions: A hard-isolated instance of the platform serves one contract; blast radius is a namespace rather than a contractId filter.

Related NFRs: blast-radius isolation, multi-tenancy.

Gated deliberately. Per AD-12, this is built only "if hard isolation becomes a real requirement" — the user-facing outcome is identical to Path B, and an operator is the single most expensive thing in the plan for a solo build. It is documented here so the option stays cheap, not because it's queued.


5. Sequence Diagram — UC-9 in Context (New Adapter Joins a Running System)

This diagram is the visual proof of the extensibility-by-addition principle: nothing about EXIST changes when NEW joins.


6. Sequence Diagram — UC-13 + UC-14 (Onboarding a Schema with No Redeploy)

The same proof, along the second axis: nothing is rebuilt when a schema joins.

Contrast this with the alternative that was rejected (AD-9): under per-schema codegen, every arrow after "persist contract definition" would instead be generate → build → publish image → deploy. Same outcome, minutes-to-hours later, with N images to patch afterwards.