Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Engineering  Module 1 – Foundations: what data engineers build

Pipeline requirements: SLAs, freshness, sizing

Translate a vague request into freshness, completeness and recovery targets with owners, size storage and peak load from measurements, and fit it on one page.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Translate a vague request into freshness, completeness and correctness targets, each with an owner
  • Set recovery point and recovery time objectives that count noticing, restoring and rerunning
  • Estimate daily volume, storage growth and peak events a second with a small script
  • Recognise when a requirement is really a quality, security or legal duty
  • Write a one-page requirements document with readers, owners and an escalation path

Before you start

What you’ll learn · Before you start

What you will learn

  • Translate a vague request into freshness, completeness and correctness targets, each with an owner
  • Set recovery point and recovery time objectives that count noticing, restoring and rerunning
  • Estimate daily volume, storage growth and peak events a second with a small script
  • Recognise when a requirement is really a quality, security or legal duty
  • Write a one-page requirements document with readers, owners and an escalation path

Before you start

On this page

The canteen operators of Campus Bite, this track’s invented campus food-ordering platform, ask for one thing: “yesterday’s payouts, early and complete”. Over 30 nights in the first program below, the payout job finished without an error every night, yet the file was both on time and complete on only 24 of them, and nobody noticed, because nobody had written down what early and complete meant.

A pipeline requirement is a promise you can measure: how fresh, complete and correct the data is when someone reads it, how much a failure may lose and how soon the data is back, how long it is kept, and what storage and throughput it needs in its busiest minute. The sections below turn the operators’ sentence into targets with owners, size the storage and the peak from measurements, and put it all on one page.

A vague request becomes targets you can measure

“Early” and “complete” cannot fail a test. Is a file published at 07:10 early? Is a file missing 30 of 40,000 orders complete? It depends on who reads the file and what they do with it, so a requirement starts with its readers. Step through how the operators’ sentence becomes targets, each with someone who answers for it:

Interactive · stage by stage From a vague ask to targets with owners Step through how one sentence from the canteen operators becomes three targets, an owner and an escalation path.
From a vague ask to targets with owners, step 7 of 7. Showing The ask, Who reads it, and when (download 06:30, pay 09:00), Freshness (published by 06:00), Completeness (99.9 % of settled orders), Correctness (totals match settlement), Owner and escalation (payments data team). In focus: Freshness, Completeness, Correctness.The askpayouts early and completeWho reads it, and whendownload 06:30, pay 09:00Freshnesspublished by 06:00Completeness99.9 % of settled ordersCorrectnesstotals match settlementOwner and escalationpayments data team

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The ask. The canteen operators ask for "yesterday's payouts, early and complete". Nobody can test that sentence: is a file at 07:10 early, and is a file missing 30 orders complete?
  2. Find the readers first. Ask who reads the file and what they do with it. The operators download it at 06:30 to check the night's payout, and finance pays them at 09:00. Their times, not the job's, decide every target.
  3. Early becomes a time. Early becomes a freshness target: the file is published by 06:00, half an hour before the first reader, on at least 29 of 30 days. The margin covers a slow night; the 29 says how often a miss is acceptable.
  4. Complete needs a count. Complete becomes a share of a count the pipeline did not produce: at least 99.9 % of the day's settled orders, counted from the payment provider's settlement report. A job that compares its output with its own input always looks complete.
  5. Right is a check too. Nobody asked for the payouts to be right, but a wrong payout costs more than a late one. The file is held back when its total differs from the settlement total, so a wrong file never reaches a canteen.
  6. Someone answers for it. Each target gets an owner who is told when it is missed, and an escalation path with times: the on-call engineer at 05:30 if nothing is published, the payments data lead at 06:00, finance operations at 06:30.
  7. Objectives, not yet an agreement. The freshness, completeness and correctness lines are service-level objectives, targets the team sets and measures. If Campus Bite promised the canteens a refund for every late file, the same targets would sit inside an SLA, an agreement with a consequence.

The scene ends with three words that Google’s SRE book keeps apart (Service Level Objectives). A service level indicator (SLI) is a measurement, such as the time the file appeared. A service-level objective: A target for a measured property of a service, with how often it must hold, such as a file published by 06:00 on at least 29 of 30 days. A promise to customers with a consequence for missing it is a service level agreement. (SLO) sets the target an SLI must meet, a single value or a range, together with how often it must hold: published by 06:00 on at least 29 of 30 days. A service level agreement (SLA) is a promise to someone outside the team with a consequence, such as a refund, when its objectives are missed.

Pipeline targets fall into a few kinds, most with examples in Google’s SRE workbook (Data Processing Pipelines):

  • data freshness: How old data is when someone reads it, or how soon after an event it can be read. Written as a target, for example that the oldest record is no older than a stated time.: how old the data may be when it is read, such as “the oldest data is no older than Y”.
  • completeness: The share of the expected records that a dataset holds by a stated time, counted against a source the pipeline does not produce, such as a provider's own report.: the share of the expected records present by a time, counted against a source the pipeline does not produce.
  • Correctness: how much wrong data is acceptable; the workbook’s example allows at most 0.1 % of invoices to be wrong in a quarter.
  • End-to-end latency: from the event to the moment a reader can use it, summed over every stage.
  • retention period: How long data is kept before it is deleted. It can be a minimum that a business or legal duty sets, and it ends with a job that deletes the older data., recovery and size, which the later sections take one at a time.

Every target needs an owner who is told when it is missed, and an escalation path with times. A target that pages nobody is a wish.

Checkpoint

Check: Targets with owners

  1. One question Which of these is a service-level objective for the payout file?

    Choose one answer.

    Show the answer: One question

    Answer: The file is published by 06:00 on at least 29 of 30 days

    An SLO is a target for a measured property of the service, with how often it must hold. A schedule is not a target, "as early as possible" cannot be measured against, and a refund for missing the target turns it into an agreement with the canteens.

Freshness and completeness are measured where the data is read

A target is only as good as its measurement. This program replays 30 nights of the payout job against the scene’s two targets: published by 06:00, and at least 99.9 % of the day’s settled orders as the payment provider’s settlement report counts them, each on 29 of 30 days. Run it and read the last five lines:

Thirty nights of the payout file against two targets Python · slo_check.py
"""Thirty days of Campus Bite's daily payout file, checked against its two targets.

Each night a job starts at 04:30, reads the payment provider's settled orders of the day before
and publishes the payout file for the canteens. The targets are measured where the file is read:

    fresh:    the file is published by 06:00
    complete: it holds at least 99.9 % of the day's settled orders, counted from the provider's
              own settlement report, not from what the job happened to read

The runs are synthetic and seeded, so every run of this program prints the same 30 days.
"""

import random

SEED = 20261017
DAYS = 30
START = 4 * 60 + 30  # the job starts at 04:30 (minutes after midnight)
DEADLINE = 6 * 60  # published by 06:00
COMPLETE = 99.9  # per cent of the settled orders
TARGET_DAYS = 29  # each target must hold on at least 29 of the 30 days


def clock(minutes):
    return f"{minutes // 60:02d}:{minutes % 60:02d}"


def simulate(rng):
    days = []
    for day in range(1, DAYS + 1):
        settled = 38_000 + int(rng.random() * 6_000)
        # Most nights every settled order has arrived before 04:30; on some, part of the
        # provider's file comes later and the job never sees it.
        late = int(settled * (0.002 + rng.random() * 0.006)) if rng.random() < 0.08 else 0
        # Most runs take 28 to 52 minutes; a slow night takes 85 to 110.
        minutes = 85 + int(rng.random() * 26) if rng.random() < 0.02 else 28 + int(rng.random() * 25)
        days.append({"day": day, "settled": settled, "in_file": settled - late, "published": START + minutes})
    return days


def main():
    days = simulate(random.Random(SEED))
    print(f"Campus Bite payout file: {DAYS} nightly runs (seeded)")
    print(f"Targets: published by {clock(DEADLINE)}, holding at least {COMPLETE} % of the day's settled orders")
    print()
    print("Days that missed a target:")
    on_time = complete = both = 0
    shares = []
    for d in days:
        share = 100 * d["in_file"] / d["settled"]
        shares.append(share)
        fresh = d["published"] <= DEADLINE
        full = share >= COMPLETE
        on_time += fresh
        complete += full
        both += fresh and full
        if not (fresh and full):
            missing = d["settled"] - d["in_file"]
            what = f"{missing:,} of {d['settled']:,} orders missing" if missing else "nothing missing"
            print(f"  day {d['day']:>2}: published {clock(d['published'])}, {share:6.2f} % complete ({what})")
    print()
    print(f"The job finished without an error:      {DAYS} of {DAYS} days")
    print(f"Published by {clock(DEADLINE)}:                     {on_time} of {DAYS} days, target {TARGET_DAYS}: {'met' if on_time >= TARGET_DAYS else 'missed'}")
    print(f"At least {COMPLETE} % complete:              {complete} of {DAYS} days, target {TARGET_DAYS}: {'met' if complete >= TARGET_DAYS else 'missed'}")
    print(f"Fresh and complete, as a reader saw it: {both} of {DAYS} days")
    print(f"Completeness averaged over the month:   {sum(shares) / DAYS:.2f} %")


main()

Output

Campus Bite payout file: 30 nightly runs (seeded)
Targets: published by 06:00, holding at least 99.9 % of the day's settled orders

Days that missed a target:
  day  6: published 05:11,  99.21 % complete (332 of 42,255 orders missing)
  day 18: published 05:10,  99.69 % complete (126 of 41,304 orders missing)
  day 21: published 05:21,  99.27 % complete (288 of 39,270 orders missing)
  day 24: published 06:07, 100.00 % complete (nothing missing)
  day 26: published 05:16,  99.45 % complete (235 of 42,590 orders missing)
  day 28: published 05:11,  99.72 % complete (114 of 40,406 orders missing)

The job finished without an error:      30 of 30 days
Published by 06:00:                     29 of 30 days, target 29: met
At least 99.9 % complete:              25 of 30 days, target 29: missed
Fresh and complete, as a reader saw it: 24 of 30 days
Completeness averaged over the month:   99.91 %

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 slo_check.py

The job finished without an error every night, and the freshness target was met: only day 24’s file was late, at 06:07. The completeness target was missed. On days 6, 18, 21, 26 and 28, part of the provider’s file arrived after the job had read it, so between 114 and 332 orders were missing from files that looked finished, and a reader who needed both saw a good file on 24 days. Averaged over the month, completeness was 99.91 %, above the target: the average hid all five short days.

Change the promise, not the data

Press Edit, set COMPLETE to 99.0 and run it again: every day passes the completeness target, and 29 files are fresh and complete. The same orders are still missing; only the promise changed. Targets come from what readers need, not from what the pipeline already does.

Three habits follow from the run:

  1. Measure at the reader’s end: when the file appeared where the operators download it, not when the job logged its last step.
  2. Count against a source the pipeline does not produce: compared with what the job read, a file always looks complete.
  3. Count the days that meet the target, as “29 of 30” does, instead of averaging over a month.

A green job is not a fresh, complete file

Wrong: alert when the payout job fails. On day 6 it succeeded at 05:11 with 332 orders missing, and nothing fired. Right: alert on the targets where the file is read: no file by 06:00, or a file holding less than 99.9 % of the settlement report’s count.

The freshness target also chooses the delivery: a nightly batch can meet “by 06:00”, while “no older than five minutes” needs a micro-batch or a stream, with the costs that the data lifecycle lesson compares.

Key idea

Measure each target where the reader meets the data, against a count the pipeline did not produce, day by day: a successful job and a good monthly average can both hide the bad days.

Checkpoint

Check: Measuring where the data is read

  1. Question 1 of 2 On day 6 the payout job finished at 05:11 without an error, and 332 of 42,255 settled orders were missing because part of the provider's file arrived late. Which alert would have fired?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A file holding less than 99.9 % of the settlement report's count

    Only a count from outside the pipeline sees orders that never reached it. The job's success, the file's time and a comparison with the job's own input all looked normal on day 6.

  2. Question 2 of 2 The target is at least 99.9 % complete on 29 of 30 days. Pipeline A is at 99.95 % every day. Pipeline B is at 100 % on 27 days and at 99.5 % on 3 days, also 99.95 % on average. Which pipelines meet the target?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only A

    Judge the target day by day and count the days that meet it. The averages are equal, but B misses on 3 days and A on none, so only A meets "29 of 30".

Recovery objectives say how much may be lost and for how long

Freshness and completeness describe an ordinary night; recovery objectives describe the worst one. NIST’s contingency planning guide defines both (SP 800-34 Rev. 1). The recovery point objective: The most data a failure may lose, measured as the time between the last point the data can be recovered to and the failure. Input kept for processing again moves that point forward. (RPO) is the point in time to which data can be recovered from the most recent backup, so it limits how much a failure may lose. The recovery time objective: The longest a system or dataset may stay unavailable after a failure before the harm is unacceptable, counting the time to notice, to restore and to process data again. (RTO) is the longest a system may stay unavailable before the harm to the business is unacceptable. Walk through one bad night of the payout tables:

Timeline · 6 events in order One lost night, read as recovery objectives Walk through the night in order; each tag says what the moment means for recovery.
  1. 00:00

    The nightly snapshot is taken restore point

    The warehouse copies the payout tables to a second location. This is the newest copy a restore can start from, so a restore can bring back the tables only as they were at midnight.

  2. 04:30

    The payout job writes the new day writes after the copy

    The job reads the day's settled orders, which arrive as files that are kept for 30 days, and writes the new payout rows into the warehouse tables.

  3. 05:05

    The volume holding the tables is lost up to 5 h 5 min lost

    Every row written since midnight was on the lost volume. Restoring the snapshot alone would lose 5 hours and 5 minutes of writes: that gap is the data loss a recovery point objective limits.

  4. 05:20

    The on-call engineer is paged detection

    Fifteen minutes pass before anyone knows. Detection is part of the recovery time: readers cannot tell a failure nobody has noticed from a slow night.

  5. 06:35

    The tables are back as they were at midnight restore done

    The restore takes 1 hour and 15 minutes. The day's payout rows are still missing, so the file cannot be published yet, and the 06:00 freshness target has already passed.

  6. 07:20

    The job runs again and the file is published recovered after 2 h 15 m

    The settled-order files were kept, so the job runs again on the same input and rebuilds every lost row: in the end no data is lost. Readers waited from 05:05 to 07:20, 2 hours and 15 minutes, because the rerun counts as recovery time too.

The timeline has 6 events, from 00:00 to 07:20.

Read the night twice. As a backup question, the newest copy was from midnight, so a restore alone would have lost 5 hours and 5 minutes of writes. As a pipeline question, nothing was lost: the settled-order files were kept for 30 days, and running the job again rebuilt every row. Kept input is a second backup when it is kept longer than the longest gap you may have to rebuild, and when a run on the same input gives the same rows, an idempotent run: A pipeline run that produces the same output for the same input, no matter how many times it executes. More in “What data engineering is, and what it is not”.

Recovering takes longer than restoring. NIST says that time spent reprocessing data must be added to the RTO, and the minutes before anyone notices count too. The readers waited 2 hours and 15 minutes, and the 06:00 target was missed, which its one-in-30 allowance covers.

Set the objectives from the readers’ deadlines. The job ends by about 05:20 and finance pays at 09:00, so an RTO of 3 hours recovers a failure at 05:20 by 08:20. With the input files kept for 30 days, the RPO is no lost settled order.

Key idea

For a pipeline, kept input meets the RPO as surely as a backup of its output, and the RTO counts every minute until readers have the data again: noticing, restoring and running the job again.

Checkpoint

Check: Recovery point and recovery time

  1. Question 1 of 2 A table is copied every 6 hours and its input is not kept, so a lost table can only be restored from the last copy. What is the best recovery point objective the team can promise, in hours of writes that may be lost?

    Type a number.

    A hint

    Think of a failure a moment before the next copy is taken.

    Show the answer: Question 1 of 2

    Answer: 6 hours

    A failure just before a copy loses almost everything written since the previous copy, close to 6 hours. With no kept input to process again, 6 hours is the best the team can promise; to promise less, copy more often or keep the input.

  2. Question 2 of 2 The payout tables are lost at 05:05 and the file is published at 07:20. Which of these count towards the recovery time? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • The 75 minutes the restore takes
    • The 15 minutes before the on-call engineer is paged
    • The 45 minutes of running the job again on the kept input

    The recovery time runs from the failure to the moment readers have the data: 15 minutes to notice, 75 to restore and 45 to rerun, 2 hours and 15 minutes in all. The gap before the failure belongs to the recovery point.

Storage is arithmetic: events, bytes, compression and days

How much space does a year of order events take? Events a day times bytes per event, divided by how many times smaller the stored layout is, times the days you keep. The events and the days come from the business; the bytes and the ratio depend on your rows, so measure them. This program writes the same 200,000 synthetic orders in three layouts of its own: CSV, JSON Lines, and a layout packed by column that stores each column apart, numbers in binary, each repeated name once and each time as the seconds since the previous order. It uses no compressor:

One order in three layouts, measured in bytes Python · layout_sizes.py
"""Bytes per order in three layouts: CSV, JSON Lines and a column-wise packed binary.

The program makes 200,000 synthetic orders of the invented Campus Bite platform (seeded, so
every run prints the same numbers), writes them in three layouts into memory and counts the
bytes. No general-purpose compressor is used: the packed layout only stores each column on
its own, numbers as binary, repeated names once, and times as the seconds since the previous
order.
"""

import csv
import io
import json
import random
from bisect import bisect
from itertools import accumulate

SEED = 20261017
ORDERS = 200_000
DAYS = 5
FIRST_DAY = "2031-03-04"  # the synthetic data's first day (UTC); the next four follow it
CAMPUSES = ["north", "south", "east", "west", "river", "hill", "lake", "park", "city", "valley", "garden", "harbour"]
STATUSES = ["delivered", "cancelled", "refunded"]
STATUS_SHARE = list(accumulate([92, 5, 3]))
# Share of the day's orders placed in each hour (breakfast, a lunch rush, dinner).
HOUR_SHARE = list(accumulate([1, 1, 0, 0, 0, 1, 2, 4, 7, 6, 5, 8, 14, 13, 6, 4, 4, 5, 7, 9, 8, 5, 3, 2]))
COLUMNS = ["order_id", "placed_at", "campus", "canteen", "rider", "status", "items", "amount"]


def arrival(r):
    """When one order is placed, in seconds from the start of the first day."""
    day = int(r() * DAYS)
    hour = bisect(HOUR_SHARE, r() * HOUR_SHARE[-1])
    # Lectures end at 12:00 and 13:00: half of those two hours' orders come in their first 10 minutes.
    if hour in (12, 13) and r() < 0.5:
        second = int(r() * 600)
    else:
        second = int(r() * 3_600)
    return day * 86_400 + hour * 3_600 + second


def make_orders(rng):
    """200,000 orders in time order. placed_at is in seconds from the start of the first day;
    amounts are whole numbers of the smallest currency unit."""
    r = rng.random
    times = sorted(arrival(r) for _ in range(ORDERS))
    orders = []
    for n, t in enumerate(times):
        status = STATUSES[bisect(STATUS_SHARE, r() * STATUS_SHARE[-1])]
        orders.append({
            "order_id": 1_000_001 + n,
            "placed_at": t,
            "campus": CAMPUSES[int(r() * len(CAMPUSES))],
            "canteen": 1 + int(r() * 60),
            "rider": 0 if status == "cancelled" else 1 + int(r() * 2_400),
            "status": status,
            "items": 1 + int(r() * 6),
            "amount": (30 + int(r() * 571)) * 100,
        })
    return orders


def stamp(t):
    """An ISO 8601 UTC time for t seconds after the start of the first day."""
    day, rest = divmod(t, 86_400)
    year, month, first = FIRST_DAY.split("-")
    return f"{year}-{month}-{int(first) + day:02d}T{rest // 3600:02d}:{rest // 60 % 60:02d}:{rest % 60:02d}Z"


def as_csv(orders, stamps):
    out = io.StringIO()
    w = csv.writer(out, lineterminator="\n")
    w.writerow(COLUMNS)
    w.writerows([o["order_id"], s, o["campus"], o["canteen"], o["rider"], o["status"], o["items"], o["amount"]] for o, s in zip(orders, stamps))
    return out.getvalue().encode("utf-8")


def as_json_lines(orders, stamps):
    encode = json.JSONEncoder(separators=(",", ":")).encode
    lines = [encode(dict(o, placed_at=s)) for o, s in zip(orders, stamps)]
    return ("\n".join(lines) + "\n").encode("utf-8")


def put_varint(out, n):
    """An unsigned number in 7-bit groups: 0-127 take one byte, 128-16,383 two, and so on."""
    while n >= 0x80:
        out.append((n & 0x7F) | 0x80)
        n >>= 7
    out.append(n)


def deltas(values):
    """The first value, then each value minus the one before it, every one as a varint."""
    out = bytearray()
    put_varint(out, values[0])
    for before, now in zip(values, values[1:]):
        put_varint(out, now - before)
    return bytes(out)


def dictionary(values):
    """The distinct names once, then one byte per row that says which name it is."""
    names = sorted(set(values))
    index = {name: i for i, name in enumerate(names)}
    head = json.dumps(names).encode("utf-8")
    out = bytearray()
    put_varint(out, len(head))
    return bytes(out) + head + bytes(index[v] for v in values)


def varints(values):
    out = bytearray()
    for v in values:
        put_varint(out, v)
    return bytes(out)


def as_packed(orders):
    """Column by column, each column a block of bytes with its length in front."""
    col = {name: [o[name] for o in orders] for name in COLUMNS}
    blocks = {
        "order_id": deltas(col["order_id"]),
        "placed_at": deltas(col["placed_at"]),
        "campus": dictionary(col["campus"]),
        "canteen": bytes(col["canteen"]),
        "rider": b"".join(r.to_bytes(2, "little") for r in col["rider"]),
        "status": dictionary(col["status"]),
        "items": bytes(col["items"]),
        "amount": varints(col["amount"]),
    }
    out = bytearray()
    head = json.dumps({"rows": len(orders), "columns": COLUMNS}).encode("utf-8")
    put_varint(out, len(head))
    out += head
    for block in blocks.values():
        put_varint(out, len(block))
        out += block
    return bytes(out), blocks


def main():
    orders = make_orders(random.Random(SEED))
    stamps = [stamp(o["placed_at"]) for o in orders]
    csv_bytes = as_csv(orders, stamps)
    json_bytes = as_json_lines(orders, stamps)
    packed, blocks = as_packed(orders)

    print(f"{ORDERS:,} synthetic Campus Bite orders over {DAYS} days (seeded)")
    print()
    print("The first order in the two text layouts:")
    print("  CSV         " + csv_bytes.split(b"\n")[1].decode())
    print("  JSON Lines  " + json_bytes.split(b"\n")[0].decode())
    print()
    print(f"{'layout':<18}{'bytes':>12}{'per order':>11}{'vs CSV':>8}")
    for name, data in [("CSV", csv_bytes), ("JSON Lines", json_bytes), ("packed by column", packed)]:
        print(f"{name:<18}{len(data):>12,}{len(data) / ORDERS:>11.1f}{len(data) / len(csv_bytes):>8.2f}")
    print()
    print("Bytes per order in the packed layout, column by column:")
    for name, block in blocks.items():
        print(f"  {name:<10}{len(block) / ORDERS:>5.2f}")
    print()
    print(f"CSV takes {len(csv_bytes) / len(packed):.1f} times the bytes of the packed layout,")
    print(f"and JSON Lines {len(json_bytes) / len(packed):.1f} times.")


main()

Output

200,000 synthetic Campus Bite orders over 5 days (seeded)

The first order in the two text layouts:
  CSV         1000001,2031-03-04T00:00:07Z,harbour,17,0,cancelled,4,17900
  JSON Lines  {"order_id":1000001,"placed_at":"2031-03-04T00:00:07Z","campus":"harbour","canteen":17,"rider":0,"status":"cancelled","items":4,"amount":17900}

layout                   bytes  per order  vs CSV
CSV                 11,989,127       59.9    1.00
JSON Lines          28,789,067      143.9    2.40
packed by column     2,153,376       10.8    0.18

Bytes per order in the packed layout, column by column:
  order_id   1.00
  placed_at  1.00
  campus     1.00
  canteen    1.00
  rider      2.00
  status     1.00
  items      1.00
  amount     2.77

CSV takes 5.6 times the bytes of the packed layout,
and JSON Lines 13.4 times.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 layout_sizes.py

One order took 59.9 bytes as CSV, 143.9 as JSON Lines, which repeats every field name on every line, and 10.8 packed by column: CSV needed 5.6 times the bytes of the packed layout, JSON Lines 13.4 times. The packed layout uses two encodings that the Parquet format defines, a dictionary for repeated values and deltas for integers (Parquet encodings). Parquet writers usually add a compression codec on top, and DuckDB’s documentation notes that lightweight codecs such as Snappy, LZ4 and zstd decompress faster than gzip (DuckDB: file formats).

Now Campus Bite’s arithmetic. A term day has about 40,000 orders, and each sends six events (placed, paid, accepted, ready, picked up, delivered) that carry the whole order row: 240,000 events of 144 bytes, or 34.6 MB a day as JSON Lines. Kept 400 days, a year of payouts for disputes plus a month to spare, they fill 13.82 GB, or 1.03 GB packed. After that, storage stops growing if a job deletes each day once it is 400 days old.

A compression ratio from someone else's data

Wrong: read that one format is ten times smaller than another and divide by 10. Right: write one real day of your own rows in the layout you will store, divide the sizes, and record the ratio and its source. Field names, repeated values and sort order change it from one dataset to the next.

These estimates use decimal units, as storage is sold (1 GB is 1,000,000,000 bytes); a tool that counts in binary units shows the same 13.82 GB as 12.87 GiB. Two tools help with the inputs:

Data Storage Converter Convert an estimate between GB and GiB, or bytes and terabytes, before you compare it with a disk or a bill. Mock Data Generator Make a day of synthetic rows in your own schema, then count their bytes as the layout program does.

Checkpoint

Check: Storage arithmetic

  1. One question Campus Bite keeps its 240,000 order events a day as CSV, at 59.9 bytes an event, uncompressed, for 400 days. About how many GB does it hold at the end (1 GB = 1,000,000,000 bytes)?

    Type a number.

    A hint

    Events a day times bytes per event times days, then divide by a billion.

    Show the answer: One question

    Answer: 5.75 GB (anything from 5.69 to 5.81 counts)

    240,000 × 59.9 = 14,376,000 bytes a day, and 14,376,000 × 400 = 5,750,400,000 bytes, about 5.75 GB. That is between the 13.82 GB of JSON Lines and the 1.03 GB packed by column.

Size for the busiest minute, not the average

Storage follows the days you keep; throughput follows the busiest minute. Students order when lectures end, so Campus Bite’s orders bunch at lunch. This program makes the arrival times of the same 200,000 orders, prints an average minute of each hour and finds the busiest minute:

When the orders arrive: by hour, and the busiest minute Python · peaks.py
"""When Campus Bite's orders arrive: the hour-by-hour profile, the busiest minute and the peak factor.

The arrival times are made exactly as in layout_sizes.py (the same seed, the same draws), so these
are the same 200,000 synthetic orders. The peak factor is the busiest minute's orders divided by
the orders of an average minute: the number a sizing estimate multiplies the average by.
"""

import random
from bisect import bisect
from collections import Counter
from itertools import accumulate

SEED = 20261017
ORDERS = 200_000
DAYS = 5
EVENTS_PER_ORDER = 6  # placed, paid, accepted, ready, picked up, delivered
HOUR_SHARE = list(accumulate([1, 1, 0, 0, 0, 1, 2, 4, 7, 6, 5, 8, 14, 13, 6, 4, 4, 5, 7, 9, 8, 5, 3, 2]))
PER_HASH = 5  # orders a minute that one # stands for


def arrival(r):
    """When one order is placed, in seconds from the start of the first day."""
    day = int(r() * DAYS)
    hour = bisect(HOUR_SHARE, r() * HOUR_SHARE[-1])
    # Lectures end at 12:00 and 13:00: half of those two hours' orders come in their first 10 minutes.
    if hour in (12, 13) and r() < 0.5:
        second = int(r() * 600)
    else:
        second = int(r() * 3_600)
    return day * 86_400 + hour * 3_600 + second


def main():
    r = random.Random(SEED).random
    times = sorted(arrival(r) for _ in range(ORDERS))
    minutes = DAYS * 24 * 60
    average = ORDERS / minutes  # orders in an average minute

    by_hour = Counter(t // 3_600 % 24 for t in times)
    by_minute = Counter(t // 60 for t in times)
    busiest, most = max(sorted(by_minute.items()), key=lambda kv: kv[1])
    day, rest = divmod(busiest, 24 * 60)

    print(f"When {ORDERS:,} synthetic orders are placed ({DAYS} days, seeded)")
    print()
    print(f"Orders a minute, hour by hour, averaged over the {DAYS} days (each # is {PER_HASH} orders a minute):")
    for hour in range(24):
        rate = by_hour[hour] / (DAYS * 60)
        print(f"  {hour:02d}:00 {rate:6.1f}  {'#' * round(rate / PER_HASH)}")
    top_hour = max(range(24), key=lambda h: by_hour[h])
    top_rate = by_hour[top_hour] / (DAYS * 60)
    factor = most / average
    print()
    print(f"Average minute:          {average:5.1f} orders")
    print(f"Busiest hour, a minute:  {top_rate:5.1f} orders ({top_hour:02d}:00 to {top_hour + 1:02d}:00), {top_rate / average:.1f} times the average")
    print(f"Busiest minute:          {most:5d} orders (day {day + 1} at {rest // 60:02d}:{rest % 60:02d}), {factor:.1f} times the average")
    print()
    print(f"Peak factor for sizing: {factor:.1f}")
    print(f"With {EVENTS_PER_ORDER} events an order: {average * EVENTS_PER_ORDER / 60:.1f} events a second on average, "
          f"at most {most * EVENTS_PER_ORDER / 60:.1f} in the busiest minute")


main()

Output

When 200,000 synthetic orders are placed (5 days, seeded)

Orders a minute, hour by hour, averaged over the 5 days (each # is 5 orders a minute):
  00:00    5.8  #
  01:00    6.0  #
  02:00    0.0  
  03:00    0.0  
  04:00    0.0  
  05:00    5.6  #
  06:00   11.2  ##
  07:00   22.8  #####
  08:00   40.4  ########
  09:00   35.5  #######
  10:00   28.4  ######
  11:00   46.5  #########
  12:00   80.7  ################
  13:00   76.1  ###############
  14:00   34.7  #######
  15:00   23.4  #####
  16:00   23.3  #####
  17:00   28.6  ######
  18:00   40.9  ########
  19:00   52.3  ##########
  20:00   46.0  #########
  21:00   28.9  ######
  22:00   17.6  ####
  23:00   11.9  ##

Average minute:           27.8 orders
Busiest hour, a minute:   80.7 orders (12:00 to 13:00), 2.9 times the average
Busiest minute:            311 orders (day 4 at 12:03), 11.2 times the average

Peak factor for sizing: 11.2
With 6 events an order: 2.8 events a second on average, at most 31.1 in the busiest minute

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 peaks.py

An average minute holds 27.8 orders and a minute of the busiest hour 80.7, but the busiest minute, 12:03 on day 4, holds 311: 11.2 times the average. That ratio is the peak factor: The busiest period's rate divided by the average rate, such as the busiest minute's events against an average minute's. Multiplying the average by it sizes a system for the peak., and an estimate multiplies the average by it; sizing from the busiest hour would plan for only 2.9 times the average. Peaks have causes you can name, such as a lecture break, a sale launch, a festival evening or a payday, and a requirement says which one it was sized for.

The estimator combines these measurements with the 400 days. Change any input in SCENARIOS and run it again:

Storage and peak events a second for three cases Python · sizing.py
"""Storage and peak load for Campus Bite's order events, from inputs you can change.

Change the numbers in SCENARIOS and run it again. Every figure it prints is a multiplication
or a division of those inputs:

    events a day        = orders a day x events per order
    raw bytes a day     = events a day x bytes per event
    stored bytes a day  = raw bytes a day / compression ratio
    stored at the end   = stored bytes a day x retention days (older days are deleted)
    average per second  = events a day / 86,400
    peak per second     = average per second x peak factor
"""

SECONDS_A_DAY = 86_400
MB = 1_000_000  # decimal units, as storage is sold: 1 GB is 1,000,000,000 bytes
GB = 1_000_000_000

# Where the inputs come from:
#   40,000 orders a day: the synthetic term days of layout_sizes.py and peaks.py (200,000 orders in 5 days).
#   6 events an order (placed, paid, accepted, ready, picked up, delivered), each carrying the whole order
#   row, so an event is the size of one row as JSON Lines: 144 bytes in layout_sizes.py.
#   13.4: how many times smaller layout_sizes.py measured the same rows packed by column.
#   400 days: a year of payouts for disputes, plus a month to spare.
#   11.2: the busiest minute against the average minute, measured by peaks.py. Using it for all six
#   events of an order is on the safe side: an order's later events come minutes after it is placed.
SCENARIOS = [
    # name,                        orders a day, events per order, bytes per event, compression, days, peak factor
    ("This year, as JSON Lines",         40_000,                6,             144,         1.0,  400,        11.2),
    ("This year, packed by column",      40_000,                6,             144,        13.4,  400,        11.2),
    ("Next year: twice the orders",      80_000,                6,             144,        13.4,  400,        11.2),
]


def size(value):
    """Bytes as MB below a gigabyte and as GB above it."""
    return f"{value / GB:,.2f} GB" if value >= GB else f"{value / MB:,.1f} MB"


for name, orders, per_order, nbytes, ratio, days, peak_factor in SCENARIOS:
    events = orders * per_order
    raw_a_day = events * nbytes
    stored_a_day = raw_a_day / ratio
    average = events / SECONDS_A_DAY
    print(name)
    print(f"  {orders:,} orders x {per_order} events x {nbytes} bytes, compression {ratio}x, kept {days} days")
    print(f"  events a day             {events:>10,}")
    print(f"  raw data a day           {size(raw_a_day):>10}")
    print(f"  stored a day             {size(stored_a_day):>10}")
    print(f"  stored after 30 days     {size(stored_a_day * 30):>10}")
    print(f"  stored after {days} days    {size(stored_a_day * days):>10}")
    print(f"  average per second       {average:>7.1f} events")
    print(f"  peak per second ({peak_factor}x)  {average * peak_factor:>7.1f} events")
    print()

print("Storage grows until the oldest day is deleted; the consumers must keep up with the peak.")

Output

This year, as JSON Lines
  40,000 orders x 6 events x 144 bytes, compression 1.0x, kept 400 days
  events a day                240,000
  raw data a day              34.6 MB
  stored a day                34.6 MB
  stored after 30 days        1.04 GB
  stored after 400 days      13.82 GB
  average per second           2.8 events
  peak per second (11.2x)     31.1 events

This year, packed by column
  40,000 orders x 6 events x 144 bytes, compression 13.4x, kept 400 days
  events a day                240,000
  raw data a day              34.6 MB
  stored a day                 2.6 MB
  stored after 30 days        77.4 MB
  stored after 400 days       1.03 GB
  average per second           2.8 events
  peak per second (11.2x)     31.1 events

Next year: twice the orders
  80,000 orders x 6 events x 144 bytes, compression 13.4x, kept 400 days
  events a day                480,000
  raw data a day              69.1 MB
  stored a day                 5.2 MB
  stored after 30 days       154.7 MB
  stored after 400 days       2.06 GB
  average per second           5.6 events
  peak per second (11.2x)     62.2 events

Storage grows until the oldest day is deleted; the consumers must keep up with the peak.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sizing.py

This year the events arrive at 2.8 a second on average and at up to 31.1 a second in the busiest minute; next year, with twice the orders, the peak is 62.2. Suppose one consumer writes an event in 20 ms, so it handles 50 a second. Start the model at this year’s peak, 31 a second, then raise the arrivals to next year’s 62:

Interactive · change it and see One consumer of 50 events a second in the busiest minute Requests arrive at random at an average rate and one server handles them one at a time. Raise the arrival rate towards the service rate and watch the time in the system.
Mean time in the system by utilisation: mean time in the system (ms) against utilisation ρ = λ/μ (%).020406080100050100150200utilisation ρ = λ/μ (%)mean time in the system (ms)time in systemservice alone 20 msnow 62 %

At 62 % utilisation a request spends 52.6 ms in the system on average, 2.6 times its 20 ms of service: the rest is waiting. Little's law: L = λW = 1.63 requests in the system.

Mean time in the system (W) 52.6 ms
Utilisation (ρ) 62 %
Mean wait before service 32.6 ms
Mean number in the system (L = λW) 1.63
The numbers as a table
Mean time in the system with μ = 50 a second
UtilisationMean time in the system
50 %40 ms
70 %66.7 ms
80 %100 ms
90 %200 ms
95 %400 ms
99 %2,000 ms

At 31 a second the consumer is busy 62 % of the time, and an event spends 52.6 ms in the system on average, 2.6 times its 20 ms of work. At 62 it gets more than it can serve, and the queue grows for as long as the burst lasts, although next year’s average would keep it busy only 5.6 ÷ 50, or 11 %, of the time. The model is simple, one consumer and random arrivals, but its warning holds for any consumer whose peak is above its capacity.

A generous margin over the average

Wrong: size for the average with a tenfold margin. Ten times this year’s 2.8 events a second is 28, below the 31.1 of the busiest minute. Right: measure the busiest minute of a real day, or of last year’s busiest day, multiply, and record the peak factor and its source.

Checkpoint

Check: Sizing for the busiest minute

  1. Question 1 of 2 A feed averages 3,000 events a minute over the day, and its busiest minute holds 9 times as many events as an average minute. How many events a second must its consumer handle in that minute?

    Type a number.

    A hint

    First the average per second, then the peak factor.

    Show the answer: Question 1 of 2

    Answer: 450 events a second

    3,000 events a minute is 50 a second on average; the busiest minute is 9 times that, 450 a second. Sizing for the average, 50 a second, would leave the consumer far behind in that minute.

  2. Question 2 of 2 Next year's estimate is 62.2 events a second in the busiest minute and 5.6 on average. One consumer handles 50 a second. What should the requirement say?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Capacity for at least 62.2 events a second, or a written limit on how long the lunch backlog may delay events

    At 62.2 a second one consumer of 50 falls behind, as the model shows past its capacity. Size for the peak, or agree in writing how long a backlog may last, and check that against the freshness target.

Not every sentence of a request is about time or size. Three things the operators, finance and the security team said about the payout file are other kinds of requirement, each with its own owner and check:

What someone said The kind of requirement What the pipeline must do
“The payouts must be right.” Quality: correctness Compare the file’s total with the settlement total, and hold the file back when they differ
“Only finance may see the canteens’ bank accounts.” Security: access Keep account numbers in a column only finance’s role can read, masked everywhere else
“Keep a year of payout records.” Retention, perhaps a legal duty Ask who requires it and why; keep the records that long, then delete them with a job

Each of these shapes the design more than a freshness target does: a correctness rule needs a second count from outside the pipeline, an access rule decides which tables may hold which columns, and a retention rule needs two jobs, one that keeps the data for the required period and one that deletes it afterwards. Exam guides list such duties beside the technical ones: Google’s Professional Data Engineer guide names legal and regulatory compliance, and AWS’s Data Engineer Associate guide includes managing data lifecycles.

A retention duty in India's data protection rules

India’s Digital Personal Data Protection Rules, 2025 (G.S.R. 846(E), Ministry of Electronics and Information Technology) contain such a duty. Rule 8(3) says that a Data Fiduciary shall retain personal data, associated traffic data and other logs of the processing for at least one year from the processing, for the purposes in the rules’ Seventh Schedule, and then erase them unless another law requires longer. Rule 6(1)(e) asks for such logs and personal data to be kept for one year to detect and investigate unauthorised access. Rule 1(4) says that rules 6 and 8, among others, come into force eighteen months after the rules’ publication in the Official Gazette. That is what the text says, not legal advice: whether it applies to a pipeline is for the organisation’s legal team, and the requirements page records their answer as its source.

A sentence that names a person, a duty or a law has an owner outside the data team: record who it is and where the duty comes from.

Checkpoint

Check: Quality, security and legal duties

  1. One question A finance manager writes: "Keep a year of payout records." What does the requirement need before it is finished?

    Choose one answer.

    Show the answer: One question

    Answer: Who requires the year and why, a rule that keeps the records that long, and a job that deletes them afterwards

    A retention sentence is often a business or legal duty in disguise. Record who requires the period and why, keep the data at least that long, and delete it with a job when the period ends.

One page holds every requirement, its owner and its reader

Everything above fits on one page, and it should: a requirement kept in a chat thread is lost by the next incident. Here is the payout file’s page, in the order an on-call engineer would read it:

Line The payout file
Readers Canteen operators download it from 06:30; finance pays from it at 09:00
Owner The payments data team
Freshness Published by 06:00, measured where the operators download it, on at least 29 of 30 days
Completeness At least 99.9 % of the day’s settled orders by the settlement report’s count, on at least 29 of 30 days
Correctness The file’s total equals the settlement total, or the file is held back
Recovery RPO: no settled order lost, as the input files are kept 30 days. RTO: 3 hours from failure to published file
Retention Payout rows kept 400 days, then deleted by a job; the source of the period recorded
Size 240,000 order events a day; 1.03 GB packed after 400 days; up to 31.1 events a second (peak factor 11.2)
Sensitive data The canteens’ bank accounts: finance’s role only, masked elsewhere
Escalation 05:30 on-call engineer if nothing is published; 06:00 payments data lead; 06:30 finance operations

Every line says what is promised, how it is measured and who hears when it is missed; a line without a measurement or an owner is unfinished. Keep the page beside the pipeline’s code, and change it before the pipeline when a reader’s needs change.

Checkpoint

Check: The one-page requirements document

  1. One question Which line of a one-page requirements document is not finished?

    Choose one answer.

    Show the answer: One question

    Answer: Freshness: the dashboard is updated regularly

    Every line says what is promised, how it is measured and who hears when it is missed. "Updated regularly" gives none of the three.

Practise: requirements for the rider payouts

Campus Bite also pays its 2,400 riders every Monday for the week before, and that pipeline has no page yet. The lesson’s programs run in your local lab too, with numbers of your own. The exercise gives the pipeline’s readers, volumes and deadlines; you write the targets and the estimates, and the sample tests recompute your storage and peak figures from your own inputs:

Exercise · Easy · Python

Write the requirements page of the rider payouts

Campus Bite pays its 2,400 riders every Monday for the deliveries of the week before. Write the pipeline's one-page requirements document as the dictionary that rider_payout_doc() in payout_doc.py returns: replace every None and fill the two lists. Times are "HH:MM" strings, percentages are numbers from 0 to 100, and sizes are in decimal GB (1 GB = 1,000,000,000 bytes).

What the team knows:

  • Readers. Riders read their weekly statement in the app from 08:00 on Monday. The bank transfer file goes to the bank at 10:00 on Monday.
  • The job. It starts at 05:00 on Monday and usually ends by 06:00. It may have to rebuild a whole week, so it needs the week's delivery events.
  • Volume. About 36,000 deliveries a day. Each delivery arrives as one event, as it happens, and becomes one payout line of about 120 bytes as JSON Lines. One sample day of lines was 10 times smaller packed by column, and its busiest minute held 11 times the deliveries of an average minute.
  • Retention. Finance keeps payout lines for three years (1,095 days), for disputes and tax questions.

Fill in:

  1. readers and escalation: lists of (time, who) pairs, in time order. The escalation must reach someone before the bank file goes at 10:00.
  2. freshness: the latest publication time and on how many of 30 runs it must hold. The file must be ready before its first reader.
  3. completeness: the smallest share of the week's deliveries the statements must cover, and what it is counted from: one of COUNT_SOURCES.
  4. correctness and owner: one sentence each.
  5. retention_days, and recovery: how many payout lines a failure may lose, how many days of input you keep, and the RTO in hours, so that a failure at 06:00 is recovered before the bank file goes.
  6. size: your inputs (never fewer deliveries, smaller lines, better compression or a smaller peak than the facts above) and your two estimates, the storage at the end of the retention period and the payout lines a second in the busiest minute. The sample tests recompute both from your own inputs and accept them within 2 %.

Starter code · payout_doc.py

"""The one-page requirements document of Campus Bite's weekly rider payouts.

Replace every None and fill the two lists. Times are "HH:MM" strings, percentages are numbers
from 0 to 100, and sizes are in decimal GB (1 GB = 1,000,000,000 bytes).
"""

# What completeness may be counted from.
COUNT_SOURCES = {"pipeline-output", "pipeline-input", "delivery-app-records"}


def rider_payout_doc():
    """Return the requirements page as a dictionary."""
    return {
        # (time, who reads it), for example ("08:00", "riders read their statements in the app")
        "readers": [],
        "owner": None,
        # (time, who is told when the file is not ready), in time order
        "escalation": [],
        "freshness": {"published_by": None, "days_per_30": None},
        "completeness": {"min_percent": None, "counted_from": None},
        "correctness": None,
        "retention_days": None,
        "recovery": {"lost_lines": None, "input_kept_days": None, "rto_hours": None},
        "size": {
            "deliveries_per_day": None,
            "bytes_per_line": None,
            "compression_ratio": None,
            "peak_factor": None,
            "storage_gb": None,
            "peak_lines_per_second": None,
        },
    }
The sample tests · test_payout_doc.py
import re

from payout_doc import COUNT_SOURCES, rider_payout_doc

TIME = re.compile(r"^([01][0-9]|2[0-3]):[0-5][0-9]$")


def minutes(hhmm):
    """'05:30' -> 330."""
    hours, mins = hhmm.split(":")
    return int(hours) * 60 + int(mins)


def nones(value, path="doc"):
    """The paths of every None left in the document."""
    if value is None:
        return [path]
    if isinstance(value, dict):
        return [p for k, v in value.items() for p in nones(v, f"{path}.{k}")]
    if isinstance(value, (list, tuple)):
        return [p for i, v in enumerate(value) for p in nones(v, f"{path}[{i}]")]
    return []


def test_every_line_is_filled():
    """fills every line of the page: no None is left"""
    doc = rider_payout_doc()
    assert nones(doc) == []
    assert isinstance(doc["owner"], str) and doc["owner"].strip()
    assert isinstance(doc["correctness"], str) and doc["correctness"].strip()


def test_readers_and_escalation_have_times():
    """names at least two readers and an escalation path in time order, before the bank file goes at 10:00"""
    doc = rider_payout_doc()
    assert len(doc["readers"]) >= 2
    for when, who in doc["readers"]:
        assert TIME.match(when), when
        assert who.strip()
    steps = doc["escalation"]
    assert len(steps) >= 2
    for when, who in steps:
        assert TIME.match(when), when
        assert who.strip()
    times = [minutes(when) for when, _ in steps]
    assert times == sorted(times)
    assert times[-1] < minutes("10:00")


def test_freshness_comes_before_the_first_reader():
    """publishes the statements before the first reader, on a stated share of the 30 runs"""
    fresh = rider_payout_doc()["freshness"]
    assert TIME.match(fresh["published_by"]), fresh["published_by"]
    assert minutes(fresh["published_by"]) < minutes("08:00")
    assert isinstance(fresh["days_per_30"], int) and 1 <= fresh["days_per_30"] <= 30


def test_completeness_is_counted_outside_the_pipeline():
    """counts completeness against the delivery app's own records"""
    complete = rider_payout_doc()["completeness"]
    assert complete["counted_from"] in COUNT_SOURCES
    assert complete["counted_from"] == "delivery-app-records"
    assert 0 < complete["min_percent"] <= 100


def test_retention_and_recovery_fit_the_readers():
    """keeps three years of lines, loses no line, keeps a week of input and recovers before 10:00"""
    doc = rider_payout_doc()
    assert doc["retention_days"] >= 1_095
    recovery = doc["recovery"]
    assert recovery["lost_lines"] == 0
    assert recovery["input_kept_days"] >= 7
    assert 0 < recovery["rto_hours"] <= 4


def test_size_inputs_are_not_optimistic():
    """sizes from the facts: no fewer deliveries, smaller lines, better compression or smaller peak"""
    size = rider_payout_doc()["size"]
    assert size["deliveries_per_day"] >= 36_000
    assert size["bytes_per_line"] >= 120
    assert 1 <= size["compression_ratio"] <= 10
    assert size["peak_factor"] >= 11


def test_storage_estimate_follows_from_the_inputs():
    """states the storage at the end of the retention period, within 2 % of its own inputs"""
    doc = rider_payout_doc()
    size = doc["size"]
    expected = size["deliveries_per_day"] * size["bytes_per_line"] / size["compression_ratio"] * doc["retention_days"] / 1_000_000_000
    assert abs(size["storage_gb"] - expected) <= 0.02 * expected


def test_peak_estimate_follows_from_the_inputs():
    """states the payout lines a second in the busiest minute, within 2 % of its own inputs"""
    size = rider_payout_doc()["size"]
    expected = size["deliveries_per_day"] / 86_400 * size["peak_factor"]
    assert abs(size["peak_lines_per_second"] - expected) <= 0.02 * expected
A hint

Start with the size, because it is arithmetic: storage is deliveries a day × bytes per line ÷ compression ratio × retention days, divided by 1,000,000,000 for GB; the peak is deliveries a day ÷ 86,400 × the peak factor. Then work back from the readers: the statements must be ready before 08:00, and a failure at 06:00 plus your RTO must still leave the bank file ready at 10:00.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then go back over the seven ideas of this lesson, starting with the one you have answered least well:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Campus Bite's contract with the canteens says: if the payout file is published after 06:30 more than twice in a month, that month's platform fee is cut by 5 %. What is this?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: A service level agreement

    An SLI measures, an SLO sets a target for the measurement, and an SLA is a promise to someone else with a consequence when the objectives are missed. Here the consequence is the fee cut.

  2. Question 2 of 14 A new reader, the canteens' food supplier, wants the payout file at 05:30 to plan the morning deliveries. The current target is by 06:00 on 29 of 30 days. What do you do first?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: Ask what the supplier does with the file and what a late one costs, then agree a target and an owner for that reader

    A requirement starts with its readers. A new reader means a new target, set from what that reader does with the data, with its own owner and escalation; only then is the pipeline changed to meet it.

  3. Question 3 of 14 Where should the payout file's freshness be measured?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: Where the operators download it, as the time the file appeared there

    Measure a target where the reader meets the data. The job's own clock describes the job, not what the readers get.

  4. Question 4 of 14 A team measures completeness as rows written divided by rows read, and its dashboard shows 100 % every night. Why does that tell the canteens nothing?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: Orders that never reached the pipeline are in neither count, so the share is 100 % whatever is missing

    Count against a source the pipeline does not produce. A pipeline compared with its own input always looks complete.

  5. Question 5 of 14 What does a recovery point objective limit?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: How much data a failure may lose, measured back from the failure to the last point you can recover to

    NIST describes the RPO as the point in time to which data can be recovered from the most recent backup, so it limits the data lost. The RTO limits how long the system stays unavailable.

  6. Question 6 of 14 A pipeline rebuilds its tables from kept input files. Some bad loads are noticed only by the weekly reconciliation, up to 7 days later, but the input files are deleted after 5 days. What does that do to the recovery point?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: Loads older than 5 days cannot be rebuilt, so keep the input longer than the 7-day gap or notice bad loads sooner

    Kept input meets the RPO only when it is kept longer than the longest gap between a bad load and its discovery. Either keep the files more than 7 days or catch bad loads before the files are deleted.

  7. Question 7 of 14 Which formula gives the storage a table holds at the end of its retention period?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: Events a day × bytes per event ÷ compression ratio × retention days

    Storage is events a day times bytes per event, divided by the compression ratio, times the days kept. The lesson's order events give 240,000 × 144 bytes = 34.6 MB a day, or 13.82 GB after 400 days as JSON Lines.

  8. Question 8 of 14 A table receives 2,000,000 events a day at 144 bytes each as JSON Lines. Packed by column it is 13.4 times smaller. How many GB does it hold after 30 days, packed (1 GB = 1,000,000,000 bytes)?

    Type a number.

    A hint

    Bytes a day, divided by the ratio, times 30.

    Show the answer: Question 8 of 14

    Answer: 0.645 GB (anything from 0.635 to 0.655 counts)

    2,000,000 × 144 = 288,000,000 bytes a day; divided by 13.4 that is about 21,500,000 bytes, and 30 days hold about 645,000,000 bytes, 0.645 GB.

  9. Question 9 of 14 The peaks program prints a peak factor of 11.2. What does that number say?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: The busiest minute held 11.2 times the orders of an average minute

    The peak factor is the busiest period's rate divided by the average rate. Multiplying the average by it gives the rate to size for: 2.8 events a second × 11.2 is about 31.1.

  10. Question 10 of 14 A team sizes its consumer from the busiest hour, 2.9 times the average, for a feed whose busiest minute is 11.2 times the average. What happens in the minutes after the lecture break?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: The arrivals are almost four times what the consumer was sized for, so a backlog builds

    Size for the busiest minute you have measured. The busiest hour of the synthetic orders was 2.9 times the average, but its busiest minute 11.2 times, so an hourly figure plans for about a quarter of the real peak.

  11. Question 11 of 14 Which of these sentences from a request is a security requirement?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: Only finance may see the canteens' bank account numbers

    Security requirements say who may see or change what. Here the answer decides which tables may hold the account numbers and where they are masked.

  12. Question 12 of 14 A retention line reads "payout rows kept 400 days". What must exist for it to be true? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 12 of 14

    Answer:

    • Storage sized for 400 days of rows
    • A job that deletes each day's rows once they are 400 days old
    • A record of who requires the period and why

    A retention line needs a source, storage for the whole period and a job that deletes what is older. A forgotten copy keeps the data longer than anyone needs it.

  13. Question 13 of 14 Which belong on a one-page requirements document? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 13 of 14

    Answer:

    • The readers and the times they use the data
    • Each target with how it is measured and how often it must hold
    • An owner and an escalation path with times

    The page holds the readers, every target with its measurement, the owner and the escalation path. Tools are chosen later to meet it.

  14. Question 14 of 14 Finance moves its payment run from 09:00 to 07:00. Which lines of the payout file's page must change, besides the readers?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: The RTO and the escalation times, which were set from the 09:00 deadline

    The page's lines depend on each other. The RTO of 3 hours was chosen so that a failure at 05:20 is recovered before 09:00; with payment at 07:00 it must fall below about 1 hour 40 minutes, and the escalation must start earlier.

Interview questions

Warm-up (fresher): how do an SLI, an SLO and an SLA fit together for a data pipeline?

An SLI, a service level indicator, is a measurement: for a daily file, the time it appeared where its readers fetch it, or the share of the expected records it holds. An SLO, a service level objective, is a target for that measurement and how often it must hold, such as published by 06:00 on at least 29 of 30 days. An SLA, a service level agreement, is a promise to a customer with a consequence when its objectives are missed, such as a refund; if nothing happens when a target is missed, it is an SLO. They stack: an agreement names objectives, and each objective is measured by an indicator. Teams usually keep their own objectives tighter than what they agree with customers, so that they see a problem before it costs money.

Two longer questions start here: one sizes a store from inputs you name yourself, the other turns an absolute demand into recovery objectives. Sketch your first step for each before you open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    A product team wants to keep every tap and screen view of its mobile app for two years, to study how features are used. The app has about 3 million daily users. Estimate the storage and the peak event rate, say which inputs you would measure first, and how you would check the estimate.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A finance lead asks that the pipeline loading card and wallet settlements into the warehouse should never lose a single row and never go down. How do you turn that into recovery objectives, what would each choice cost, and how would you show that the objectives are met?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Over 30 nights the payout job never failed. On how many was its file both on time and complete?

    On 24. Nobody had written down what early and complete meant, so the 6 bad nights went unnoticed.

  2. What does a requirement need before it becomes an objective you can check?

    It starts from the people who read the data and names a measurement, a target, how often the target must hold, and an owner who is told when it is missed.

  3. Where and how should freshness and completeness be measured?

    Where readers meet the data, against a count the pipeline did not produce, such as a provider's own report, and day by day. A successful job and a good monthly average can both hide a bad day.

  4. What do the RPO and the RTO of a pipeline limit?

    The RPO caps the data a failure is allowed to cost; the RTO caps how long readers go without it. Kept input plus an idempotent rerun meets the RPO, while noticing, restoring and rerunning all count toward the RTO.

  5. How do you estimate the storage a dataset needs?

    Multiply events per day by bytes per event, divide by a compression ratio measured on one real day of your own rows, and multiply by the days you keep it.

  6. Why is a tenfold margin over the average a poor throughput target?

    The busiest minute can still exceed it: ten times this year's 2.8 events a second is 28, below the 31.1 measured in the busiest minute. Size for a measured peak and write down the peak factor and its source.

  7. Which other duties tend to hide inside a request for data?

    Quality, security and legal duties. Each one gets an owner and a check, on the same one-page document as the targets and the escalation path.

Key takeaways

  • A requirement starts with its readers and becomes an objective when it names a measurement, a target, how often it must hold and an owner who hears when it is missed.
  • Freshness and completeness are measured where the data is read, against a count the pipeline did not produce, and judged day by day rather than as an average.
  • The RPO limits what a failure may lose and the RTO how long readers wait; kept input with an idempotent rerun meets the first, and noticing, restoring and rerunning all count in the second.
  • Storage is events a day times bytes per event, divided by a compression ratio measured on your own rows, times the days you keep.
  • Throughput is sized for the busiest minute you have measured, with the peak factor and its source written down.
  • Quality, security and legal duties hide inside requests, and each gets an owner and a check on the same one-page document as the targets and the escalation path.

Terms in this lesson

completeness
The share of the expected records that a dataset holds by a stated time, counted against a source the pipeline does not produce, such as a provider's own report.
data freshness
How old data is when someone reads it, or how soon after an event it can be read. Written as a target, for example that the oldest record is no older than a stated time.
peak factor
The busiest period's rate divided by the average rate, such as the busiest minute's events against an average minute's. Multiplying the average by it sizes a system for the peak.
recovery point objective
The most data a failure may lose, measured as the time between the last point the data can be recovered to and the failure. Input kept for processing again moves that point forward.
recovery time objective
The longest a system or dataset may stay unavailable after a failure before the harm is unacceptable, counting the time to notice, to restore and to process data again.
retention period
How long data is kept before it is deleted. It can be a minimum that a business or legal duty sets, and it ends with a job that deletes the older data.
service-level objective
A target for a measured property of a service, with how often it must hold, such as a file published by 06:00 on at least 29 of 30 days. A promise to customers with a consequence for missing it is a service level agreement.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.