Blog / Technical / Kafka Event-Driven Architecture — Flow Diagrams

Kafka Event-Driven Architecture — Flow Diagrams

flowchart TD subgraph EventStore ["EventStore (append-only log)"] E1["v1: UserRegistered {email: alice@example.com}"] E2["v2: EmailVerified {verified_by: email_link}"] E3["v3: ProfileUpdated {display_name: Alice...

kafka-event-drivendocs

1. Event Sourcing — State Reconstruction

flowchart TD subgraph EventStore ["EventStore (append-only log)"] E1["v1: UserRegistered\n{email: alice@example.com}"] E2["v2: EmailVerified\n{verified_by: email_link}"] E3["v3: ProfileUpdated\n{display_name: Alice Smith}"] E4["v4: ProfileUpdated\n{bio: Senior ML engineer}"] E1 --> E2 --> E3 --> E4 end subgraph Replay ["replay_state(aggregate_id)"] S0["initial_state = {}"] S1["state after v1:\n{email: alice, status: pending}"] S2["state after v2:\n{email_verified: true, status: active}"] S3["state after v3:\n{display_name: Alice Smith}"] S4["state after v4:\n{bio: Senior ML engineer}"] S0 --> S1 --> S2 --> S3 --> S4 end E1 -.apply handler.-> S1 E2 -.apply handler.-> S2 E3 -.apply handler.-> S3 E4 -.apply handler.-> S4 subgraph PointInTime ["Point-in-Time Restore"] PIT["replay_state(up_to_version=2)\nreturns state after EmailVerified"] end E2 -.-> PIT

2. Outbox Relay — Guaranteed At-Least-Once Delivery

flowchart LR subgraph Application ["Application (single transaction)"] DomainWrite["Write domain record\ne.g. UPDATE orders SET status='created'"] OutboxWrite["INSERT INTO outbox\n(topic, key, value, published=false)"] DomainWrite -.same TX.-> OutboxWrite end subgraph OutboxRelay ["OutboxRelay (background thread)"] Poll["get_unpublished(limit=100)"] Publish["producer.produce(topic, key, value)"] Mark["mark_published(entry_id)"] Poll --> Publish --> Mark Mark -.loop every poll_interval_s.-> Poll end subgraph Kafka Topic["Kafka Topic"] end OutboxWrite --> Poll Publish --> Topic style DomainWrite fill:#d4edda style OutboxWrite fill:#d4edda style Publish fill:#cce5ff style Topic fill:#cce5ff

Crash scenarios handled:

Crash point Outcome
Before OutboxWrite Domain TX rolled back → no event
After OutboxWrite, before Publish Relay picks up on restart → event published
After Publish, before Mark Relay republishes → at-least-once (idempotent consumers handle dedup)
After Mark Happy path completed

3. DLQ Retry with Exponential Backoff

flowchart TD Consume["Consumer.poll()"] --> Process{"Process\nMessage"} Process -- success --> Commit["commit()"] Process -- failure --> DLQ["route_to_dlq(msg, error)"] Commit --> NextMsg["next message"] DLQ --> AddToDLQ["DLQHandler.add_to_dlq()\nretry_count=0\nnext_retry_at = now + 2^0"] subgraph DLQLoop ["DLQHandler.process_dlq() — periodic"] Ready{"is_ready_for_retry?"} Retry["Re-process message"] Success["Remove from DLQ ✓"] ReQueue["Increment retry_count\nnext_retry_at = now + 2^retry_count\nRe-queue"] Quarantine["Quarantine\n(retry_count >= max_retries)"] Ready -- Yes --> Retry Ready -- No --> SkipForNow["Skip (still in backoff)"] Retry -- success --> Success Retry -- failure, retries remaining --> ReQueue Retry -- failure, max retries hit --> Quarantine end AddToDLQ --> Ready style DLQ fill:#fff3cd style Quarantine fill:#f8d7da style Success fill:#d4edda

Backoff schedule (backoff_base=2.0, max_retries=3):

Retry Delay before retry Total wait
0 → 1 2^0 = 1s 1s
1 → 2 2^1 = 2s 3s
2 → 3 2^2 = 4s 7s
3 QUARANTINE

4. ML Inference Pipeline

flowchart LR subgraph Events ["Event Stream"] UE["Topic: user_events\nclick / purchase / view"] end subgraph FeaturePipeline FP_C["MockKafkaConsumer\ngroup: feature-pipeline"] FP_U["FeatureStore.update()\nclick_count, purchase_count,\nsession_count, last_event_ts"] FP_FS["FeatureStore\n(in-memory dict)"] FP_C --> FP_U --> FP_FS end subgraph InferencePipeline IP_C["MockKafkaConsumer\ngroup: inference-pipeline"] IP_F["FeatureStore.get(user_id)"] IP_M["MockModel.predict(features)\nscore = sigmoid(w·x + b)"] IP_P["MockKafkaProducer\nproduce(predictions)"] IP_C --> IP_F --> IP_M --> IP_P end subgraph MonitoringPipeline MP_C["MockKafkaConsumer\ngroup: monitoring-pipeline"] MP_PSI["DriftDetector.compute_psi()\nreference vs current scores"] MP_A["PSI Alert if PSI ≥ 0.10"] MP_C --> MP_PSI --> MP_A end UE --> FP_C UE --> IP_C FP_FS --> IP_F IP_P --> MP_C style UE fill:#e2f0fb style MP_A fill:#fff3cd

5. A/B Model Routing Decision Flow

flowchart TD Event["Incoming Event\n{user_id: 'user-042'}"] --> Hash subgraph ABRouter Hash["MD5(user_id.encode())\ntake first 4 bytes as uint32\nbucket = uint32 % 100"] Compare{"bucket < model_v2_traffic_pct\n(e.g. 20)?"} Hash --> Compare end Compare -- Yes: bucket 0–19 --> V2["Model v2\n(challenger, 20%)"] Compare -- No: bucket 20–99 --> V1["Model v1\n(control, 80%)"] V1 --> Score["Prediction Score"] V2 --> Score Score --> Produce["produce(predictions, user_id, score, model_id)"] subgraph Properties P1["Deterministic:\nsame user_id → same model every time"] P2["Configurable:\nchange model_v2_traffic_pct in config.yaml"] P3["Stateless:\nno session storage or sticky routing required"] end

6. Consumer Group and Offset Flow

flowchart LR subgraph Topic ["Topic: user_events (4 partitions)"] P0["P0: [0,1,2,3,4]\nHWM=5"] P1["P1: [0,1,2]\nHWM=3"] P2["P2: [0,1,2,3]\nHWM=4"] P3["P3: [0,1]\nHWM=2"] end subgraph ConsumerGroup ["Consumer Group: feature-pipeline"] CG_O["Committed offsets:\nP0→3, P1→3, P2→2, P3→0"] LAG["Lag:\nP0=2, P1=0, P2=2, P3=2\nTotal=6"] CG_O --> LAG end subgraph PollLoop Poll["poll() → round-robin\nacross assigned partitions"] Process["process message"] Commit["commit() → advance\ncommitted offset"] Poll --> Process --> Commit --> Poll end P0 --> CG_O P1 --> CG_O P2 --> CG_O P3 --> CG_O style LAG fill:#fff3cd

7. PSI Drift Detection Computation

flowchart TD Ref["Reference Scores\n(training distribution)\nn=200 samples"] Cur["Current Scores\n(production window)\nn=100 samples"] Ref --> Bin1["Bin into 10 equal buckets\n[0.0, 0.1), [0.1, 0.2), ..., [0.9, 1.0]"] Cur --> Bin2["Bin into same 10 buckets"] Bin1 --> Pct1["ref_pct_i = count_i / total_ref"] Bin2 --> Pct2["cur_pct_i = count_i / total_cur"] Pct1 --> PSI_Formula["PSI = Σ (cur_pct_i - ref_pct_i)\n × ln(cur_pct_i / ref_pct_i)"] Pct2 --> PSI_Formula PSI_Formula --> Classify{"PSI value?"} Classify -- "< 0.10" --> Stable["Stable ✓\nNo action needed"] Classify -- "0.10 – 0.25" --> Moderate["Moderate Drift ⚠\nInvestigate"] Classify -- "> 0.25" --> Major["Major Drift ✗\nRetrain model"] style Stable fill:#d4edda style Moderate fill:#fff3cd style Major fill:#f8d7da

8. Windowing — Tumbling vs Session

gantt title Event Timeline: Tumbling (300s) vs Session (30s gap) dateFormat X axisFormat %s section User A Tumbling Window 0 :0, 300 Tumbling Window 1 :300, 300 section Session Windows (User A) Session 1 (3 events) :active, 10, 60 Session 2 (2 events) :active, 200, 40 Session 3 (4 events) :active, 350, 100

Key differences:

Property Tumbling Window Session Window
Duration Fixed (e.g. 300s) Variable (activity-based)
Boundaries Clock-aligned Inactivity-driven
Use case Time-series metrics User behaviour grouping
Memory O(window_size) O(active_sessions)
Ordering Requires event timestamps Requires event timestamps