Appearance
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
| Actor | Kind | Description |
|---|---|---|
| HR Staff | Human, primary | Enters 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 Designer | Human, primary | Defines 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 Operator | Human, primary | Operates and maintains the middleware: monitors health, manages the DLQ, provisions instances. |
| Downstream Target System | System, secondary | PostgreSQL, the payroll CSV feed, and any future target — receives fan-out from adapters. |
| Kafka Broker | System, supporting | Durable transport; not a "user" but modeled where its behavior (ordering, redelivery) drives a use case. |
| New Adapter Developer | Human, secondary | Engineer 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
| ID | Name | Primary Actor | Release |
|---|---|---|---|
| UC-1 | Submit Record to a Contract | HR Staff / Record Submitter | 1 (interns) / 4 (any contract) |
| UC-2 | View Records | HR Staff / Record Submitter | 1 |
| UC-3 | Edit Record | HR Staff / Record Submitter | 7 |
| UC-4 | Delete Record | HR Staff / Record Submitter | 7 |
| UC-5 | Fan-out to Database Target | System (postgres adapter) | 1 (typed) / 5 (config-driven) |
| UC-6 | Fan-out to File Target | System (csv adapter) | 1 (typed) / 6 (config-driven) |
| UC-7 | Recover from Transient Target Failure | System | 1 |
| UC-8 | Quarantine Poison Message | System | 1 |
| UC-9 | Add a New Adapter Type | Adapter Developer | 6 |
| UC-10 | Monitor System Health & Consumer Lag | Platform Operator | 1 (basic) / 8 (dashboards) |
| UC-11 | Review and Replay DLQ Messages | Platform Operator | 8 |
| UC-12 | Configure Active Targets | Platform Operator | 6 |
| UC-13 | Define a Contract via UI | Integration Designer | 6 |
| UC-14 | Attach an Adapter via UI | Integration Designer | 6 |
| UC-15 | Provision a Contract Instance | Platform Operator | 9 (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
formsforinternsand nothing in the flow changes.
| Field | Detail |
|---|---|
| Actor | HR Staff (generically: Record Submitter) |
| Goal | Get a new record reliably distributed to every target attached to its contract, with a single submission. |
| Preconditions | UI is reachable; Source Service is running; the contract exists in the Contract Registry (UC-13); its topics exist. |
| Trigger | Submitter fills out and submits the contract's form. |
Main flow:
- HR Staff enters intern details in the UI and submits.
- UI performs client-side validation and sends
POST /contracts/interns/records(Release 1–3:POST /interns). - Source Service loads the
internscontract (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 compiledCreateInternRequest). - Source Service generates
recordId(UUID) andoccurredAt, and derivesnaturalKeyper the contract's key strategy (forinterns: theinternIdfield). - 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). - Source Service publishes to
interns.created, keyed bynaturalKey. - Source Service returns
202 Acceptedwith therecordId. - UI displays a submission confirmation to HR Staff.
Alternate flows:
- 3a. Payload fails contract validation: Source Service returns
400 Bad Requestwith 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.
contractIddoesn'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;
500returned; 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
| Field | Detail |
|---|---|
| Actor | HR Staff (generically: Record Submitter) |
| Goal | See previously submitted records for a contract and their submission status. |
| Preconditions | At least the Source Service's read path is available. |
Main flow:
- HR Staff navigates to the records view.
- UI calls
GET /contracts/interns/records(Release 1–3:GET /interns). - Source Service returns the list of submitted records (from its own read model / query store — not by reading adapter targets, preserving loose coupling).
- 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)
| Field | Detail |
|---|---|
| Actor | HR Staff (generically: Record Submitter) |
| Goal | Correct or update details of an already-submitted record. |
| Preconditions | The record already exists (has a prior created event); the contract declares an update-style record type. |
Main flow:
- HR Staff opens an existing record in the UI and edits fields.
- UI sends
PUT /contracts/interns/records/{naturalKey}. - Source Service validates against the contract and builds an update-style envelope (new
recordId, samenaturalKey,occurredAtrefreshed). - Source Service publishes to
interns.updated, keyed bynaturalKey(same key as the originalcreate, guaranteeing ordering). postgresadapter upserts on the contract's declared natural key —ON CONFLICT (contract_id, natural_key) DO UPDATEon the genericrecordstable, or the shaped table's own key column in shaped mode. (This is the one guarantee that genuinely generalized:ON CONFLICT (intern_id) DO UPDATEwas always "upsert on the declared natural key," it just had only one declaration.)csvadapter 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)
| Field | Detail |
|---|---|
| Actor | HR Staff (generically: Record Submitter) |
| Goal | Remove a record from all downstream targets (e.g., offboarding an intern). |
Main flow:
- HR Staff triggers delete in the UI.
- UI sends
DELETE /contracts/interns/records/{naturalKey}. - Source Service publishes a tombstone envelope to
interns.deleted, keyed bynaturalKey. The payload is minimal by design — a tombstone needs the key, not the data. postgresadapter deletes (or soft-deletes, per retention policy) the row matching(contract_id, natural_key).csvadapter rebuilds the CSV snapshot, omitting the record.- Every step logged with
recordId/contractId/traceIdfor 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
| Field | Detail |
|---|---|
| Actor | postgres adapter (system) |
| Goal | Persist every canonical event to PostgreSQL exactly-once in effect, despite at-least-once delivery — for any contract attached to this adapter. |
| Trigger | New message available on a subscribed topic ({contract}.created, or .updated/.deleted in Release 7) for the db-adapter consumer group. |
Main flow:
- Adapter consumes the message.
- 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+).
- Adapter checks whether the envelope's
contractIdis attached to it and resolves that attachment's target mapping from the registry (Release 5+; before that, the mapping is compiled in and onlyinternsexists). - Adapter maps the envelope + payload to a SQL row — the generic
recordsrow by default, or a shaped table per the attachment's config. - Adapter executes
INSERT ... ON CONFLICT (record_id) DO NOTHING(orON CONFLICT (contract_id, natural_key) DO UPDATEfor update-style events). - Adapter commits the Kafka offset.
Alternate flows:
- 3a.
contractIdis 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 generalization — recordId 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
| Field | Detail |
|---|---|
| Actor | csv adapter (system) |
| Goal | Append every canonical event to the contract's CSV file exactly-once in effect. |
Main flow:
- Adapter consumes the message.
- Adapter confirms the
contractIdis attached to it and resolves the attachment's file path + column mapping (Release 6+; before that,interns.csvis compiled in). - Adapter checks the dedup store: has this
recordIdalready been written? - If not: adapter maps the envelope + payload to a CSV line and appends it, then records the
recordIdin the dedup store. - Adapter commits the Kafka offset.
Alternate flows:
- 3a.
recordIdalready 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
| Field | Detail |
|---|---|
| Actor | Any adapter (system) |
| Goal | Guarantee no data loss when a target is temporarily unreachable. |
| Trigger | A write to the target throws a classified-retriable error (timeout, connection refused, etc.). |
Main flow:
- Adapter attempts to write to the target; write fails.
- Adapter classifies the failure as retriable.
- Adapter does not commit the Kafka offset.
- Adapter retries with bounded backoff (e.g., up to 3 attempts).
- If a retry succeeds, adapter commits the offset and processing resumes normally.
- 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
| Field | Detail |
|---|---|
| Actor | Any adapter (system) |
| Goal | Prevent one unprocessable message from blocking every message behind it. |
| Trigger | A message fails processing and is classified non-retriable, or a retriable failure exhausts its bounded retry count. |
Main flow:
- Adapter attempts to process the message; it fails.
- Adapter classifies the failure as non-retriable (e.g., schema violation, unmappable data) — or retries are exhausted.
- Adapter publishes the original message plus error metadata (error type, stack summary, timestamp, adapter name,
contractId) toiip.dlq. - 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)
| Field | Detail |
|---|---|
| Actor | Adapter Developer |
| Goal | Extend 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:
- 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.
- Developer assigns the adapter its own consumer group.
- Developer registers the new type in the adapter catalog, declaring the config schema an attachment must supply (e.g.
webhookneeds endpoint + auth). - Developer deploys the new adapter as an independent service.
- Existing services (UI, Source Service, other adapters) require no changes or redeploys.
- 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
| Field | Detail |
|---|---|
| Actor | Platform Operator |
| Goal | Know at a glance whether the platform is healthy and whether any consumer is falling behind. |
Main flow (Release 1 — baseline):
- Operator queries each service's Actuator
/healthand/metrics. - Operator opens Kafka UI to inspect consumer group lag per adapter.
Main flow (Release 8 — dashboards):
- Operator opens the Grafana dashboard.
- 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.
- 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)
| Field | Detail |
|---|---|
| Actor | Platform Operator |
| Goal | Inspect quarantined messages, fix the root cause, and safely reprocess them. |
Main flow:
- Operator opens the admin dashboard's DLQ view.
- Dashboard lists DLQ messages grouped by contract / error type / adapter, non-destructively read from
iip.dlq. - Operator diagnoses and fixes the root cause (code fix, data correction, or a contract correction via UC-13).
- Operator selects one or more messages and triggers replay.
- Replay tool re-publishes the original envelope to its own contract's source topic (
{contractId}.created/.updated/.deletedas appropriate) — the destination is read off the envelope, not configured per replay. - Replay tool marks the DLQ entry as replayed (audit trail — not deleted, to preserve history).
- 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)
| Field | Detail |
|---|---|
| Actor | Platform Operator |
| Goal | Enable/disable which adapter attachments are active without redeploying the core platform. |
Main flow:
- Operator toggles an adapter attachment's
enabledflag in the Contract Registry (via the control-plane UI or API). - Adapters read their attachments from the registry at startup and on a refresh interval.
- 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)
| Field | Detail |
|---|---|
| Actor | Integration Designer |
| Goal | Register a new schema (fields, types, natural key, record types) so the platform can accept and route its records with no redeploy. |
| Preconditions | Control-Plane API and Contract Registry are running. |
| Trigger | A new kind of record needs onboarding (e.g. forms). |
Main flow:
- 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. - UI validates field/key definitions client-side (e.g. the key strategy must reference declared fields).
- UI sends
POST /contracts. - Control-plane persists the definition to the Contract Registry and provisions the contract's topics.
- The parameterized source-service picks up the new contract (on refresh or next boot).
- The contract is now live for submissions (UC-1).
Alternate flows:
- 3a.
contractIdalready 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):
400with 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)
| Field | Detail |
|---|---|
| Actor | Integration Designer |
| Goal | Fan a contract's records out to a target by instantiating a catalog adapter type as config — not by writing one. |
| Preconditions | The contract exists (UC-13); the desired adapter type exists in the catalog (UC-9). |
Main flow:
- Designer picks an adapter type (
postgres/csv/webhook) for a contract. - Designer enters that type's required target config — table + write mode, file path + columns, or endpoint + auth.
- UI sends
POST /contracts/{id}/adapters. - Registry stores the attachment.
- 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)
| Field | Detail |
|---|---|
| Actor | Platform Operator |
| Goal | Run a schema in its own isolated pod set, rather than sharing the platform's services. |
| Preconditions | Kubernetes + the IIP operator are deployed; and hard isolation has become a real, stated requirement. |
Main flow:
- Operator (or the control-plane UI) writes an
IIPInstancecustom resource naming the contract. - 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.
- 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.