Lakehouse & Ingestion at Scale
Databricks, Delta and moving billions of events a day
Lakehouse fundamentals on Databricks
After this you can explain the lakehouse model and why Cursor uses it.
A lakehouse is cheap object-store storage that behaves like a warehouse. Once you can say exactly which warehouse guarantees Delta Lake adds on top of plain files, the rest of this module is mechanics.
Classic architectures forced a choice. A data lake gave you cheap storage and open formats but no transactions, so concurrent writers corrupted tables and readers saw half-written data. A warehouse gave you ACID and schemas but locked your data in a proprietary engine and charged warehouse prices for every byte. The lakehouse keeps the lake's storage and adds the warehouse's guarantees through a metadata layer.
On Databricks that layer is Delta Lake. The data still lands as Parquet files in an object store like S3, but every table carries a transaction log - an ordered set of JSON commit files in a _delta_log directory that records which Parquet files belong to the table at each version.
- ACID commits
- Each write is an atomic log entry; readers see a complete version or the previous one, never a partial table
- Time travel
- Query any past version (
VERSION AS OF) because the log knows the file set at every commit - replay and audit for free - Schema enforcement
- Writes that don't match the table schema are rejected at commit, so bad data never lands silently
- Schema evolution
- Add columns or widen types deliberately (
mergeSchema) without rewriting history - Concurrency
- Optimistic concurrency on the log lets many writers commit safely instead of corrupting files
The log is the table. The Parquet files are just storage it points at.
Unity Catalog: the governance layerone place to grant, trace and audit
Tables alone don't make a platform governable. Unity Catalog sits above the workspaces and gives every table a three-level name - catalog.schema.table - with access control, lineage and audit attached to that name rather than to a cluster.
- Centralized RBACRole-Based Access Control. Granting permissions by role rather than configuring each person individually.. Grant on catalogs, schemas or tables once; it holds across every workspace and compute type instead of being re-pasted per cluster.
- Column and row controls. Column masks and row filters let you expose a table while hiding PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive. columns or restricting rows by group - the lever you reach for with sensitive product data.
- Lineage and audit. Unity Catalog tracks table- and column-level lineage and logs access, which is how you answer "where did this number come from" and "who read this PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive." without a forensic dig.
Why lakehouse over a warehouse hereone copy, three very different consumers
Cursor's data serves BI dashboards, product analytics and ML/agent training from the same tables. A warehouse-only stack would force you to copy data out to a separate object store every time the ML side needed raw Parquet, doubling storage and creating two sources of truth that drift. The lakehouse keeps a single governed copy that Spark, SQL and training jobs all read directly.
- Storage cost
- Raw data lake
- Lowest (object store)
- Classic warehouse
- Highest (proprietary)
- Lakehouse (Delta + UC)
- Low (object store)
- ACID / schema
- Raw data lake
- None
- Classic warehouse
- Strong
- Lakehouse (Delta + UC)
- Strong (via Delta log)
- Open formats
- Raw data lake
- Yes (Parquet)
- Classic warehouse
- No (locked-in)
- Lakehouse (Delta + UC)
- Yes (Parquet under Delta)
- Serves ML on raw files
- Raw data lake
- Yes, but ungoverned
- Classic warehouse
- Awkward, needs export
- Lakehouse (Delta + UC)
- Yes, governed in place
- Governance / lineage
- Raw data lake
- Bolt-on, weak
- Classic warehouse
- Strong but engine-bound
- Lakehouse (Delta + UC)
- Unity Catalog, cross-engine
| Raw data lake | Classic warehouse | Lakehouse (Delta + UC) | |
|---|---|---|---|
| Storage cost | Lowest (object store) | Highest (proprietary) | Low (object store) |
| ACID / schema | None | Strong | Strong (via Delta log) |
| Open formats | Yes (Parquet) | No (locked-in) | Yes (Parquet under Delta) |
| Serves ML on raw files | Yes, but ungoverned | Awkward, needs export | Yes, governed in place |
| Governance / lineage | Bolt-on, weak | Strong but engine-bound | Unity Catalog, cross-engine |
The lakehouse is the only column that serves BI and ML from one governed copy at object-store cost.
When asked "why a lakehouse," don't list features - name the consumer conflict it resolves. "We serve BI, product analytics and agent-training data from the same telemetry. A warehouse would make me export raw files for the ML side and run two sources of truth; Delta plus Unity Catalog gives one governed copy at object-store cost." That ties the choice to Cursor's actual workload, which is the senior signal.
Don't call Delta "just Parquet." The Parquet files are inert without the transaction log - drop the _delta_log and you've lost ACID, time travel and the file set itself. Conflating the two reads as surface knowledge; the log is what makes it a table.
Takeaway. A lakehouse is object-store storage with warehouse guarantees: the Delta transaction log adds ACID, time travel and schema control and Unity Catalog governs one copy that BI and ML share.
Self-check
QWhy does Cursor prefer a lakehouse over a classic warehouse for this role's data?
The medallion (bronze/silver/gold) architecture
After this you can design layered pipelines from raw to serving.
Medallion is one rule: each layer raises trust and shape and you never skip a layer to save a hop. Bronze preserves, silver cleans, gold serves.
The point of layering is that different consumers need different contracts. A replay or audit needs raw fidelity; a dashboard needs a clean conformed table; an agent feature store needs a narrow purpose-built shape. Collapsing those into one table means every consumer fights every other consumer's requirements.
Append-only, untouched first- and third-party data
Preserve full fidelity for replay and reprocessing
Minimal transform: just land it durably and fast
Cleaned, deduplicated, schema-validated, typed
Conformed keys and consistent grain across sources
The trustworthy backbone most consumers build on
Business marts and ML/agent feature tables
Shaped for a specific consumer and query pattern
Aggregated, denormalized or feature-engineered
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Each layer rests on the one below - and stays rebuildable from it.
What happens at each hopthe transforms that earn the layer
- 1Land to bronze. Write incoming events append-only with idempotent keys; do not parse or reshape beyond what's needed to store them durably. This is the replay buffer for everything downstream.
- 2Promote to silver. Deduplicate on event keys, enforce and cast the schema, drop or quarantine malformed rows and conform identifiers so a
user_idmeans the same thing across sources. - 3Build gold. Aggregate into business marts or engineer agent/ML features from silver - narrow tables shaped for one query pattern, rebuildable from silver at any time.
The JD's "optimize the raw/bronze data layer for performance, reliability and cost at TB-to-PB scale" lives at the first hop. Bronze is where billions of events a day actually land, so its file layout and write path decide whether the rest of the platform is cheap or expensive to read.
Each layer gets its own contractSLAs, freshness and access differ by layer
- Layer
- Bronze
- Freshness target
- Near-real-time landing
- Access
- Engineers only (raw, may contain PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive.)
- Quality contract
- Complete and replayable; little validation
- Layer
- Silver
- Freshness target
- Minutes to hours
- Access
- Broad internal, PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive. masked via UC
- Quality contract
- Deduplicated, schema-valid, conformed
- Layer
- Gold
- Freshness target
- Tied to the dashboard/feature SLA
- Access
- Wide self-serve, role-scoped
- Quality contract
- Business-correct, tested, documented
| Layer | Freshness target | Access | Quality contract |
|---|---|---|---|
| Bronze | Near-real-time landing | Engineers only (raw, may contain PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive.) | Complete and replayable; little validation |
| Silver | Minutes to hours | Broad internal, PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive. masked via UC | Deduplicated, schema-valid, conformed |
| Gold | Tied to the dashboard/feature SLA | Wide self-serve, role-scoped | Business-correct, tested, documented |
Mapping SLAs and access to layers is how you give self-serve without exposing raw PII.
Every gold table must be rebuildable from silver and silver from bronze. That property is what makes a backfill or a bug fix safe: you reprocess from the layer below instead of patching serving tables in place. If a gold table can't be regenerated, it has become a second source of truth and a liability.
Don't let consumers query bronze directly to "get fresher data." Bronze has duplicates, unvalidated schemas and raw PIIPersonally Identifiable Information. Data that can identify a person (names, emails, SSNs); regulated and sensitive.; every team that reads it reinvents cleaning logic inconsistently. Freshness pressure is real, so the answer is a faster silver path, not opening the raw layer to everyone.
Takeaway. Bronze preserves raw fidelity for replay, silver conforms into the trusted backbone, gold serves consumer-shaped marts and features - and every layer keeps it rebuildable from the one below.
Self-check
QWhich layer carries the JD's mandate to 'optimize the raw/bronze layer for performance, reliability and cost at TB-to-PB scale,' and why is its file layout so consequential?
Ingestion patterns: batch, streaming, CDC
After this you can choose the right ingestion pattern per source.
The pattern follows the source, not the other way around. High-volume product events want a stream; SaaS systems of record want a connector. Pick wrong and you either melt a connector or build a streaming stack for a nightly Salesforce pull.
Cursor's two source families pull in opposite directions. First-party product telemetry is a firehose of editor and client events that arrives continuously. Third-party business data - billing, CRM, support - lives in SaaS systems you read on a schedule or via change capture.
- Source
- Product telemetry (events)
- Pattern
- Streaming or micro-batch
- Why
- Continuous high volume; you want it in bronze within seconds/minutes
- Tooling
- Kafka/Kinesis → Spark Structured Streaming / Auto Loader
- Source
- SaaS systems of record (CRM, billing)
- Pattern
- Connector batch + CDC
- Why
- Source is a database/API, not a stream; you want low-effort, reliable pulls
- Tooling
- Fivetran/Airbyte connectors, scheduled or log-based CDC
- Source
- Operational app databases
- Pattern
- CDC (log-based)
- Why
- Capture inserts/updates/deletes without full re-scans
- Tooling
- Debezium-style log capture → stream → bronze
| Source | Pattern | Why | Tooling |
|---|---|---|---|
| Product telemetry (events) | Streaming or micro-batch | Continuous high volume; you want it in bronze within seconds/minutes | Kafka/Kinesis → Spark Structured Streaming / Auto Loader |
| SaaS systems of record (CRM, billing) | Connector batch + CDC | Source is a database/API, not a stream; you want low-effort, reliable pulls | Fivetran/Airbyte connectors, scheduled or log-based CDC |
| Operational app databases | CDC (log-based) | Capture inserts/updates/deletes without full re-scans | Debezium-style log capture → stream → bronze |
Match the pattern to the source's nature: streams for firehoses, connectors/CDC for systems of record.
When you're standing up a connector to a system of record, two Cursor moves save a round of guesswork. Microsoft SQL Server has a SQL Server VS Code extension, and you can also connect it over MCPModel Context Protocol. A standard that lets an AI agent pull in context from outside the repo, like Jira tickets or internal docs. so the agent runs queries against the source directly while you design the pull - ask the agent how to set either up. And when you hit an "is this even supported" question, chat with the Cursor docs in-product rather than guessing.
You can actually chat with them directly and ask questions like, 'Is GitHub Enterprise supported?' And then it will actually go through, look through our docs, and give you an answer using the same underlying models that our agent uses.
Delivery semantics: the trade-off you must nameexactly-once is mostly idempotency in disguise
At billions of events a day, true end-to-end exactly-once is expensive and often unnecessary. The pragmatic pattern is at-least-once delivery plus idempotent writes, so duplicates are guaranteed to arrive but harmless because the write path collapses them.
Producers may resend; you dedup on a stable event key
Cheap, resilient, the common production choice
Bronze keeps duplicates; silver dedups deterministically
Stronger guarantee, more coordination and cost
Needs transactional sinks / Kafka transactions end to end
Reserve for flows where a dup is genuinely unacceptable
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Pick the default for the firehose; reserve the stronger guarantee for flows where a dup is a real error.
Idempotency needs a key that's stable across retries - a producer-assigned event id or a deterministic hash of the payload plus timestamp. Then a MERGE into the Delta target or a dropDuplicates window in silver makes a replayed event a no-op rather than a double count.
Schema registry and evolutionkeep producers and the lakehouse from breaking each other
Hundreds of client builds emit events independently, so a producer will add a field or change a type without warning. A schema registry gives every event type a versioned schema and enforces compatibility rules at publish time, so a breaking change is rejected at the producer instead of corrupting bronze.
- Backward compatibility lets new consumers read old data - add optional fields, never repurpose an existing one.
- Forward compatibility lets old consumers tolerate new data - they ignore unknown fields rather than crash.
- Bronze stays permissive, silver stays strict. Land what arrives, then enforce the contract on the way to silver so a producer hiccup degrades one table, not the platform.
Backpressure, dead-letter queues and replay are not optional at billions a day - they're the difference between a bad hour and a bad week. A poison message with no dead-letter path stalls the whole consumer group; no backpressure and a producer spike overruns your buffer and you drop data silently. Always say where malformed events go and how you replay them.
If asked "exactly-once or at-least-once," don't pick a side abstractly. Say: "At-least-once with idempotent writes for the telemetry firehose, because dups are cheap to collapse on a stable event key and the coordination cost of true exactly-once isn't worth it at this volume. I'd reserve stronger semantics for flows like billing where a duplicate is a real error." Tying the choice to the specific flow is the senior answer.
Takeaway. Stream the telemetry firehose, connect/CDC the systems of record and lean on at-least-once + idempotent writes with a schema registry, dead-letter path and replay rather than chasing exactly-once everywhere.
Self-check
QWhy is 'at-least-once delivery plus idempotent writes' usually the right default for high-volume telemetry and what makes it work?
Designing for billions of events per day
After this you can reason quantitatively about throughput and layout at the JD's stated scale.
"Billions a day" is a number you should be able to convert to events per second in your head, then turn into partitions, file sizes and consumer counts. Interviewers watch for whether you do the arithmetic or wave at it.
Start with the math, out loud. A day is 86,400 seconds, so one billion events a day averages roughly 11,600 events/sec; five billion is about 58,000/sec. Traffic isn't flat, so apply a peak multiplier - developer usage clusters in working hours, so a 3-5x peak over the daily average is a reasonable planning assumption.
- Daily average
- ~58,000 events/sec (5e9 / 86,400)
- Peak (4x)
- ~230,000 events/sec - size partitions and consumers for this, not the average
- Kafka partitioning
- Enough partitions that per-partition throughput stays under broker/consumer limits, with headroom
- Consumer parallelism
- One consumer per partition ceiling; scale partitions before you can scale consumers
- Target Delta file size
- ~128MB-1GB compacted, not thousands of tiny streaming files
Always size for peak. Sizing for the daily average guarantees you fall over at 9am.
Partitioning without skewthe layout that enables pruning and parallelism
Partition bronze by event date (and maybe hour) so time-bounded queries and retention prune whole directories instead of scanning everything. Resist partitioning on a high-cardinality column like user_id directly - millions of tiny partitions is its own disaster. For clustering within a partition, use Z-ordering or Delta liquid clustering on the columns you actually filter on.
- Partition on low-to-medium cardinality time columns (date, hour) for pruning and retention.
- Watch for skew - a single hot tenant or a null key can pile most rows into one partition and one task; salt the key or use liquid clustering to spread the load.
- Cluster, don't over-partition - Z-order or liquid clustering on filter columns gives pruning without exploding the partition count.
The small-file problemthe single most common scale failure
Streaming and micro-batch writes naturally produce many small files - every micro-batch flushes a fresh Parquet file. Thousands of tiny files wreck read performance, because each one costs a metadata lookup and an open and they inflate cost on object stores that charge per request.
- 1Compact on a schedule. Run Delta
OPTIMIZEto coalesce small files into right-sized ones (~128MB-1GB), so downstream reads open a handful of files, not thousands. - 2Turn on auto-compaction / optimized writes. Let the writer target larger files during ingest so you generate fewer small files in the first place.
- 3Vacuum stale files. Remove tombstoned files past the retention window to reclaim storage, but keep enough history for time-travel replay.
The bronze write path and the silver/gold transforms must be independent jobs with their own compute. If a transform is slow or fails, ingest keeps landing raw events durably; if events spike, transforms drain the backlog at their own pace. Couple them and a single slow transform stalls the whole platform during exactly the traffic spike you built bronze to absorb.
Don't quote "billions a day" and then propose a layout sized for the daily average. The number that breaks you is the working-hours peak, which can be several times the average. Stating a peak multiplier and sizing partitions and consumers against it is the difference between a design that holds and one that pages you at 9am.
Takeaway. Convert the daily number to peak events/sec, partition on time (not high-cardinality keys), kill the small-file problem with OPTIMIZE and auto-compaction and decouple ingest from transform so a spike never stalls the platform.
Self-check
QWhy is the small-file problem so damaging at streaming scale and what are the two complementary fixes?
Spark performance internals
After this you can tune Spark jobs that interviewers will probe.
Most Spark slowness is one of three things: a shuffle you didn't need, skew you didn't handle or a join strategy you didn't pick. Name the bottleneck precisely and the fix is usually obvious.
A shuffle is Spark moving data across the network so rows that need to be together - same join key, same group - land on the same executor. It's the expensive operation: wide transforms like join, groupBy and distinct trigger it and a shuffle that spills to disk because it won't fit in memory is where jobs go to die.
- Symptom
- A few tasks run forever, rest finish fast
- Likely cause
- Data skew (hot key / null key)
- Lever
- Salt the key, AQE skew join, skew hint
- Symptom
- Huge shuffle read/write, disk spill
- Likely cause
- Unnecessary or oversized wide transform
- Lever
- Filter/aggregate earlier, broadcast the small side, raise shuffle partitions
- Symptom
- Join is slow, one side is small
- Likely cause
- Shuffle join where a broadcast would do
- Lever
- Broadcast join (auto via AQE or hint)
- Symptom
- Repeated scans of the same DataFrame
- Likely cause
- Recomputing instead of reusing
- Lever
- Cache/persist the reused dataset
- Symptom
- Job is just CPU-bound on scans
- Likely cause
- No vectorization
- Lever
- Enable Photon for vectorized execution
| Symptom | Likely cause | Lever |
|---|---|---|
| A few tasks run forever, rest finish fast | Data skew (hot key / null key) | Salt the key, AQE skew join, skew hint |
| Huge shuffle read/write, disk spill | Unnecessary or oversized wide transform | Filter/aggregate earlier, broadcast the small side, raise shuffle partitions |
| Join is slow, one side is small | Shuffle join where a broadcast would do | Broadcast join (auto via AQE or hint) |
| Repeated scans of the same DataFrame | Recomputing instead of reusing | Cache/persist the reused dataset |
| Job is just CPU-bound on scans | No vectorization | Enable Photon for vectorized execution |
Match the symptom to the cause out loud - that's what the Spark probe is actually testing.
Skew: the hot-key problemone tenant ruins the whole stage
Skew is when one key has far more rows than the others, so a single task processes most of the data while the rest sit idle. At Cursor a handful of huge enterprise tenants can dominate a groupBy user_id or org_id. The fixes are salting (append a random suffix to the hot key to split it across tasks, then re-aggregate) and Adaptive Query Execution, which detects skewed partitions at runtime and splits them automatically.
Broadcast vs shuffle joinsthe single highest-value join decision
Small side fits in executor memory
Ship the small table to every executor - no shuffle of the big one
AQE auto-picks it under the broadcast threshold
Both sides large; neither broadcasts
Shuffle both by key so matches co-locate
Correct but costly - minimize the data shuffled
-- AQE: re-optimizes the plan with runtime stats. SET spark.sql.adaptive.enabled = true; SET spark.sql.adaptive.skewJoin.enabled = true; -- split skewed partitions SET spark.sql.adaptive.coalescePartitions.enabled = true; -- shrink tiny shuffle partitions -- Force a broadcast when you know the small side fits: SELECT /*+ BROADCAST(dim) */ f.*, dim.name FROM fact f JOIN dim ON f.dim_id = dim.id;
Caching and cluster sizing for costwhere the bill is set
Cache a DataFrame only when you reuse it multiple times in a job; caching something read once just wastes memory you needed for the shuffle. Photon is Databricks' vectorized C++ engine that speeds up scans and aggregations on the same cluster, so it's a near-free win for SQL-heavy ETL.
- Cores
- Match parallelism to shuffle-partition count; too few cores serializes tasks
- Memory
- Size to avoid shuffle spill on your largest stage, not your smallest
- Autoscaling
- Scale workers with the job's stages; cap the max so a runaway job can't run up the bill
- Spot vs on-demand
- Spot for fault-tolerant batch (huge savings, can be preempted); on-demand for SLA-bound or driver nodes
When a Spark job is slow in the interview, don't jump to "add more workers." Ask for the Spark UI signal first: "Is one task much slower than the rest?" (skew) or "How big is the shuffle read?" (a wide transform to push down or a join to broadcast). Diagnosing from the stage view before touching cluster size is exactly the seniority signal - throwing hardware at skew just buys idle executors.
Spot instances are a real cost win on fault-tolerant batch, but never put the driver or an SLA-critical streaming job on pure spot - a preemption kills the driver and the whole job dies. The clean answer is spot for stateless batch workers, on-demand for the driver and anything with a freshness SLA.
Takeaway. Diagnose from the Spark UI first: skew (one slow task → salt/AQE), shuffle (push down or broadcast the small side), then size the cluster and use spot only where preemption is safe.
Self-check
QA daily aggregation by org_id has stalled: 198 of 200 tasks finished in two minutes, two have run for forty. What's happening and how do you fix it?
Mini system-design: product telemetry ingestion
After this you can walk an end-to-end ingestion design out loud.
This is the design round in miniature: take Cursor's editor telemetry from the client to feature tables and narrate the trade-off at every hop. The structure below is one you can reuse cold.
Open by restating requirements before you draw anything - it's the move that separates a structured answer from a brain-dump.
- Volume
- Billions of Cursor client/editor events per day; size for the working-hours peak
- Loss tolerance
- Low - telemetry feeds product metrics and agent training, so dropped events skew both
- Replayability
- Must be able to reprocess from raw after a bug fix or schema change
- Latency
- Minutes to bronze is fine; sub-second isn't the goal for analytics/training
- Cost
- Bounded - the layout has to keep storage and read cost sane at this volume
The pipeline, hop by hopclient to feature tables
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The two gates are where bad data is stopped: idempotency on the way into bronze, schema enforcement on the way into silver.
- 1Client → ingest gateway. A thin gateway authenticates, validates the envelope, attaches a server-side receive timestamp and assigns or checks an event id for idempotency. It buffers and sheds load gracefully rather than failing the editor.
- 2Gateway → stream (Kafka). Publish to topics partitioned for the peak throughput from section 4. The stream is the durable buffer that decouples bursty clients from downstream compute.
- 3Stream → bronze Delta (idempotent). Spark Structured Streaming / Auto Loader appends raw events to bronze, partitioned by event date/hour, with a
MERGEor dedup window on the event id so replays don't double-count. - 4Bronze → silver (dedup/conform). Deduplicate, enforce the registered schema, cast types, resolve late and out-of-order events with watermarks and conform identifiers. This is the trusted backbone.
- 5Silver → gold / feature tables. Build the product-metric marts and the agent/ML feature tables, each shaped for its consumer and rebuildable from silver.
At each hop, call out the same four concerns the interviewer is listening for: partitioning, compaction, schema evolution and dead-letter handling. Saying "malformed events go to a dead-letter topic with the raw payload and error and I can replay them after a fix" is worth more than any extra box on the diagram.
SLAs and how you'd alert on eachfreshness and completeness are the two that matter
- SLA
- Freshness
- Definition
- Silver lags bronze by < N minutes
- Alert signal
- Max event-time watermark vs now exceeds threshold
- SLA
- Completeness
- Definition
- Daily event count within expected band
- Alert signal
- Volume anomaly vs trailing baseline (per source)
- SLA
- Lag
- Definition
- Consumer keeps up with the stream
- Alert signal
- Kafka consumer lag rising / not draining
- SLA
- Schema health
- Definition
- No rejected/quarantined spike
- Alert signal
- Dead-letter rate above baseline
| SLA | Definition | Alert signal |
|---|---|---|
| Freshness | Silver lags bronze by < N minutes | Max event-time watermark vs now exceeds threshold |
| Completeness | Daily event count within expected band | Volume anomaly vs trailing baseline (per source) |
| Lag | Consumer keeps up with the stream | Kafka consumer lag rising / not draining |
| Schema health | No rejected/quarantined spike | Dead-letter rate above baseline |
Tie every SLA to a concrete metric you can alert on - vague 'monitor it' answers don't land.
Failure modes and recoverythe part that proves you've run one of these
Watermarks bound how late you'll wait in silver
Idempotent MERGE on event id collapses duplicates
Bronze retains raw, so a wider replay is always possible
Registry rejects breaking changes at the producer
Bronze permissive, silver strict - one table degrades, not all
Quarantine bad rows, alert, fix, replay from bronze
Stream buffers the spike; transforms drain at their pace
Autoscale consumers up to the partition ceiling
Alert when lag stops draining, not on every blip
Ingest keeps landing bronze independently
Backfill silver/gold from bronze once recovered
No data lost because raw is durable and replayable
“Client events hit a thin ingest gateway that stamps a server timestamp and an event id, then publish to Kafka topics partitioned for our peak - Kafka is the durable buffer so a client spike never touches downstream compute. Structured Streaming appends to bronze Delta, partitioned by event date, with an idempotent MERGE on the event id so replays don't double-count. Bronze→silver dedups, enforces the registered schema and uses watermarks for late events; malformed rows go to a dead-letter topic I can replay after a fix. Gold and the agent feature tables are derived from silver and fully rebuildable. I'd SLA on freshness and completeness, alerting on watermark lag and a volume anomaly per source.”
Close the design by naming what you'd cut for v1 and what you'd add at 10x. "For v1 I'd ship gateway → Kafka → bronze → silver with idempotent writes and a dead-letter path and defer liquid clustering and the feature store until the access patterns are real." Volunteering the staging plan signals the pragmatic, ship-fast judgment the role is filtering for, not gold-plating.
Takeaway. Restate requirements, draw client → gateway → Kafka → bronze → silver → gold/features and at every hop name partitioning, compaction, schema evolution and dead-lettering - then close with SLAs, failure recovery and a v1-vs-10x cut.
Self-check
QIn your telemetry design, why is Kafka (the stream) placed between the ingest gateway and bronze rather than writing events straight to Delta?