Production-Ready Event Ingestion Handler: Validation, Idempotent Writes, DLQ, Metrics
ScribdSoftware Engineer · OnsiteLast updated
Tests your ability to write production-grade ingestion code that enforces strict schema validation, guarantees idempotent persistence, handles retries and Dead-Letter Queue (DLQ) routing, and emits operational metrics.
Problem Statement
You are building an event ingestion service for a content platform that receives document interaction events from upstream streaming sources (e.g., Kafka or webhooks). Due to network retries, upstream producers frequently send duplicate payloads or malformed data.
Implement an EventIngestionHandler that processes incoming raw event payloads safely, writes valid records to a database, and handles failures according to production standards.
Requirements
-
Validation:
event_id: non-emptystr(strict type; e.g., numbers or other types must fail).user_id: positiveint(user_id > 0). Strings like"123"must be rejected to prevent type coercion bugs in downstream relational schemas.event_type:strbelonging to an allowed set:{"view", "bookmark", "highlight", "download"}.timestamp: positiveintrepresenting epoch milliseconds (timestamp > 0).payload: must be adict(can be empty).- If any validation rule fails, record a metric, publish the raw event to the
DeadLetterQueuewith an error reason, and return status"REJECTED".
-
Idempotency:
- If an
event_idhas already been written to the database, do not write it again. Emit a duplicate metric and return status"DUPLICATE".
- If an
-
Persistence and Retries:
- Call
database.insert(event). If a transientDatabaseErroris raised, retry up tomax_retries(default 2 retries, total 3 attempts). If all attempts fail, log the error, emit a failure metric, publish to theDeadLetterQueue, and return status"FAILED".
- Call
-
Metrics:
- Emit metric counts for:
"events.received","events.success","events.duplicate","events.validation_failed", and"events.dlq_routed".
- Emit metric counts for:
Interfaces
class Database:
def exists(self, event_id: str) -> bool: ...
def insert(self, event: dict) -> None: ... # May raise DatabaseError
class DeadLetterQueue:
def send(self, raw_event: dict, reason: str) -> None: ...
class MetricsClient:
def increment(self, metric_name: str, count: int = 1) -> None: ...
class DatabaseError(Exception):
pass
class EventIngestionHandler:
def __init__(self, db: Database, dlq: DeadLetterQueue, metrics: MetricsClient, max_retries: int = 2):
...
def handle(self, raw_event: dict) -> str:
"""
Processes raw_event and returns one of: 'SUCCESS', 'DUPLICATE', 'REJECTED', 'FAILED'
"""
...
Example 1
event = {
"event_id": "evt_001",
"user_id": 42,
"event_type": "view",
"timestamp": 1700000000000,
"payload": {"document_id": "doc_999"}
}
# Result: 'SUCCESS'
# Database receives insert, metrics 'events.received' and 'events.success' incremented.
Example 2
event = {
"event_id": "evt_002",
"user_id": "42", # String instead of int
"event_type": "bookmark",
"timestamp": 1700000000000,
"payload": {}
}
# Result: 'REJECTED'
# DLQ receives payload with reason 'invalid user_id: expected int'.
# Metrics 'events.received', 'events.validation_failed', and 'events.dlq_routed' incremented.
Hints
Hint 1
Use strict type checking with `type(v) is int` rather than `isinstance(v, int)` if you want to avoid matching booleans (in Python, `isinstance(True, int)` is True).
Hint 2
Separating the validation logic into a dedicated helper function makes the main orchestration loop concise and easy to unit test.
Hint 3
Ensure that database insertion and duplicate checks are resilient to race conditions; in SQL databases, an upsert or `INSERT ... ON CONFLICT DO NOTHING` is often used instead of separate check-then-insert steps.
Follow-up questions
- How would you handle race conditions where two identical events arrive concurrently across two different worker processes at the exact same millisecond?
- If the DLQ itself fails or becomes unreachable, what strategy should the ingestion handler use to avoid silent data loss?
- How would you adapt this handler to process batches of 1,000 events efficiently without issuing 1,000 separate database queries or metric calls?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.