Skip to content

Data Model & Contract Document

System: Intern Integration Platform (IIP) Related docs: Architecture · Use Cases · Implementation Plan

This document defines the canonical event contract, its evolution rules, and how each target's storage model derives from it. The canonical model is the single source of truth for shape — no adapter or the source service should ever invent fields that aren't defined here first.


1. Canonical Record Schema

FieldTypeRequiredNotes
recordIdUUID (string)yesUnique per event (not per intern). Generated server-side by the Source Service. Primary idempotency key for adapters.
internIdstringyesBusiness identifier for the intern (e.g., INT001). Stable across the intern's lifecycle. Used as the Kafka partition key.
firstNamestringyes
lastNamestringyes
emailstring (email format)yes
collegestringyes
departmentstringyes
mentorstringno
startDatedate (YYYY-MM-DD)yes
statusenum: ACTIVE, COMPLETED, WITHDRAWNyesExtendable via schema evolution (§5).
createdAttimestamp (ISO-8601 UTC)yesSet server-side at publish time.
updatedAttimestamp (ISO-8601 UTC)conditionalPresent on intern.updated events only (Release 5).
traceIdUUID (string)yes (Release 5+)Correlates all events belonging to one logical user action across the pipeline for end-to-end log reconstruction.

Example (intern.created):

json
{
  "recordId":   "5c1f2e4a-3b7d-4e9a-9c2f-8a6d7e1b0c3f",
  "internId":   "INT001",
  "firstName":  "John",
  "lastName":   "Doe",
  "email":      "john@example.com",
  "college":    "MIT",
  "department": "Data Engineering",
  "mentor":     "Alice",
  "startDate":  "2026-08-01",
  "status":     "ACTIVE",
  "createdAt":  "2026-07-21T14:10:00Z",
  "traceId":    "b6e2a9d0-1c4f-4a2e-8e7a-2f9d3c5b1a70"
}

No service other than the Source Service (producer) and the adapters (consumers) ever sees this record in a target-specific form — the canonical shape is what crosses every service boundary.


2. Event Types (Lifecycle)

EventTopicIntroducedPayload delta vs. intern.created
Createdintern.createdRelease 1Baseline schema above (minus updatedAt).
Updatedintern.updatedRelease 5Adds updatedAt; carries the full current state of the record (not a diff) — simplest for consumers to apply, avoids reconstructing state from partial deltas.
Deletedintern.deletedRelease 5Minimal tombstone payload: recordId, internId, deletedAt, traceId. No need to carry full record data for a deletion.

3. DLQ Message Envelope

Messages routed to intern.dlq wrap the original payload rather than replacing it, so nothing is lost and the failure is fully diagnosable.

FieldTypeDescription
originalTopicstringTopic the message originated from (e.g., intern.created).
originalPartitionintPartition of the original message.
originalOffsetlongOffset of the original message, for correlation with broker logs.
originalKeystringThe original internId key.
originalPayloadbytes / JSONThe unmodified original message (preserved exactly).
errorTypestringClassification, e.g. SCHEMA_VIOLATION, UNMAPPABLE_DATA, RETRY_EXHAUSTED.
errorMessagestringHuman-readable error summary.
failedAdapterstringWhich consumer group/adapter produced this DLQ entry.
attemptCountintHow many processing attempts were made before quarantine.
quarantinedAttimestampWhen it was routed to the DLQ.
replayedbooleanSet true by the DLQ Replay Tool (Release 7) once reprocessed — entry is kept for audit, not deleted.
replayedAttimestamp, nullableWhen replay occurred.

4. Target Storage Models

4.1 PostgreSQL (Database Adapter)

  • record_id carries a UNIQUE constraint — this is what makes ON CONFLICT (record_id) DO NOTHING a correct idempotency guard for intern.created redelivery.
  • intern_id also carries a UNIQUE constraint (one current-state row per intern) — this is the conflict target for intern.updated (ON CONFLICT (intern_id) DO UPDATE).
  • INTERN_AUDIT_LOG (Release 5) is append-only by design — it's the audit trail, so it should not be upserted; every event (created/updated/deleted) adds a row here regardless of what happens to the INTERNS row.

4.2 CSV File (File Adapter)

Columns (Release 1, create-only):

csv
record_id,intern_id,first_name,last_name,email,college,department,mentor,start_date,status,created_at

Dedup index: a local key-value store (RocksDB / SQLite / embedded H2) mapping recordId -> written (bool), consulted before every append and updated after every successful write. This is what makes the naturally non-idempotent "append a line" operation idempotent in effect.

Release 5 shift — snapshot rebuild instead of pure append: once intern.updated/intern.deleted exist, the file adapter maintains its keyed store as current state per internId (not just a seen-set), and on every event rebuilds interns.csv from that current state:

This resolves the append-only-vs-mutable tension named in the original spec (§7) by treating the CSV as a projection of current state, not a raw event log — option 2 from the original design brief, adopted platform-wide rather than deferred.


5. Schema Evolution Rules

Schema Registry compatibility mode: BACKWARD (default) — a new schema can read data written with the previous schema. This is the correct default for this platform because adapters are deployed independently (principle: independent deployability) and must not break when the Source Service ships a schema change first.

ChangeAllowed under BACKWARD compatibility?Notes
Add a new optional field with a defaultYesSafe, most common evolution (e.g., adding traceId).
Add a new required fieldNo, unless a default is suppliedWould break consumers built against the old schema reading new data — must supply a default.
Remove a field consumers depend onNoBreaking; requires a coordinated multi-step migration (deprecate, stop-writing, then remove).
Widen a type (e.g., intlong)Generally yes (Avro promotion rules)Verify against the specific serialization format's promotion table.
Rename a fieldNo (treated as remove + add)Use aliasing support if the serialization format provides it, or a two-phase add-new/remove-old rollout.
Add a new enum value to statusYes, if consumers treat unknown values defensivelyConsumers should not switch exhaustively without a default case.

Governance process: every schema change is proposed as a PR against the schema definition, validated by the Schema Registry's compatibility check in CI before merge — an incompatible change fails the build, not production.


6. Canonical Model → Target Mapping Summary

Canonical fieldPostgreSQL columnCSV columnNotes
recordIdrecord_id (UNIQUE)record_idIdempotency key everywhere
internIdintern_id (UNIQUE)intern_idPartition key / natural key
firstNamefirst_namefirst_name
lastNamelast_namelast_name
emailemailemail
collegecollegecollege
departmentdepartmentdepartment
mentormentor (nullable)mentor (blank if absent)
startDatestart_datestart_date
statusstatusstatus
createdAtcreated_atcreated_at
updatedAtupdated_at (nullable)(reflected via snapshot rebuild, no dedicated column)Release 5
traceId(logged, not persisted in the row)(logged, not persisted)Kept in structured logs / audit log table, not the projection tables

This table is the contract every adapter's transform is unit-tested against (see Implementation Plan §7 — Engineering Practices).