ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯЧДя╕П рдзрдбрд╛ 04 тАФ Data model рдЖрдгрд┐ database рдЪреА рдирд┐рд╡рдб: рдЖрдзреА рдкреНрд░рд╢реНрдирд╛рдВрдЪреЗ model рдХрд░рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЧДя╕П рдзрдбрд╛ 04 тАФ Data model рдЖрдгрд┐ database рдЪреА рдирд┐рд╡рдб: рдЖрдзреА рдкреНрд░рд╢реНрдирд╛рдВрдЪреЗ model рдХрд░рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 04 ┬╖ рдорд╛рдЧреЗ: lesson-03-api-design ┬╖ рдкреБрдвреЗ: lesson-05-caching


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ03, рдЖрдгрд┐ рднрд╛рдЧ 1 рдЪреА рд╢реЗрд╡рдЯрдЪреА рдкрд╛рдпрд░реА: data. Access patterns (app data рд▓рд╛ рд╡рд┐рдЪрд╛рд░рдгрд╛рд░ рддреЗ рдкреНрд░рд╢реНрди) рдпрд╛рдВрдЪреА рдпрд╛рджреА рдХрд░рд╛, рддреНрдпрд╛рдВрдЪреА рдЙрддреНрддрд░реЗ рджреЗрдгрд╛рд░реЗ table рдЖрдгрд┐ index рд▓рд┐рд╣рд╛, рдЖрдгрд┐ рдордЧрдЪ store рдЪрд╛ рдкреНрд░рдХрд╛рд░ рдирд┐рд╡рдбрд╛ тАФ рдореВрд▓рддрдГ relational, рдЖрдгрд┐ key-value store, search engine, vector index, object storage рдХрд┐рдВрд╡рд╛ time-series store рдлрдХреНрдд рдПрдЦрд╛рджреНрдпрд╛ access pattern рд▓рд╛ рддреНрдпрд╛рдЪреА рдЧрд░рдЬ рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ. design/designs.py рдордзреАрд▓ choose_store() рдЧрд░рдЬрд╛рдВрдЪреЗ store рдордзреНрдпреЗ рд░реВрдкрд╛рдВрддрд░ рдХрд░рддреЗ; design/demo.py рдордзреАрд▓ data() рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рдЪреА рдирд┐рд╡рдб рдЫрд╛рдкрддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреЗ рдХрд╛рд░реНрдпрд╛рд▓рдп рдПрдХ рдореЛрдареА рдиреЛрдВрджрд╡рд╣реА ЁЯУТ рдареЗрд╡рддреЗ. рддрд┐рдЪреНрдпрд╛рд╕рд╛рдареА рдХрдкрд╛рдЯ рд╡рд┐рдХрдд рдШреЗрдгреНрдпрд╛рдЖрдзреА рджреАрдкрд┐рдХрд╛ рд╡рд┐рдЪрд╛рд░рддреЗ: "рд▓реЛрдХ рдпрд╛рдд рдХрд╛рдп рд╢реЛрдзрдгрд╛рд░ рдЖрд╣реЗрдд?"

рдкрд╣рд┐рд▓реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рд╕рд╛рдареА рддреА рдкрд╛рдиреЗ рдЖрдзреА рд╡рд░реНрдЧрд╛рдиреБрд╕рд╛рд░, рдордЧ рддрд╛рд░рдЦреЗрдиреБрд╕рд╛рд░ рдХреНрд░рдорд╛рдиреЗ рд▓рд╛рд╡рддреЗ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рд╕рд╛рдареА рдПрдХ рдЦреВрдг-рдкрдЯреНрдЯреА ЁЯУС. рдЖрддрд╛ рдХрд╛рд░рдХреВрди 3A рдЪреА рдкрдЯреНрдЯреА рдЙрдШрдбрддреЗ рдЖрдгрд┐ рд╡рд░рдЪреНрдпрд╛ 20 рд╡рд╛рдЪрддреЗ. рддреА рдкрдЯреНрдЯреА рдореНрд╣рдгрдЬреЗ index.

"рдХреНрд░реАрдбрд╛ рджрд┐рди" рдкреНрд░рд╢реНрдирд╛рд╕рд╛рдареА рд╕рд╛рдзреА рдиреЛрдВрджрд╡рд╣реА рд╡рд╛рдИрдЯ тАФ рдХрд╛рд░рдХреБрдирд╛рд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рди рд╡рд╛рдЪрд╛рд╡реЗ рд▓рд╛рдЧреЗрд▓. рдореНрд╣рдгреВрди рд▓реЛрдХ рддреЛ рдЦрд░реЛрдЦрд░ рдЦреВрдк рд╡рд┐рдЪрд╛рд░рдд рдЕрд╕рддреАрд▓, рддрд░ рджреАрдкрд┐рдХрд╛ рдкреБрд╕реНрддрдХрд╛рдЪреНрдпрд╛ рдорд╛рдЧреЗ рдПрдХ рд╡реЗрдЧрд│реА рд╢рдмреНрджрд╕реВрдЪреА рдЬреЛрдбрддреЗ: "рдХреНрд░реАрдбрд╛ рджрд┐рди тЖТ рдкрд╛рдиреЗ 12, 88, 301". рддреЛ рдореНрд╣рдгрдЬреЗ search engine.

Photos рдиреЛрдВрджрд╡рд╣реАрдд рдЕрдЬрд┐рдмрд╛рдд рдЬрд╛рдд рдирд╛рд╣реАрдд. рддреЗ рдПрдХрд╛ рдХреЛрдареАрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд ЁЯЧГя╕П рдЬрд╛рддрд╛рдд, рдЖрдгрд┐ рдиреЛрдВрджрд╡рд╣реАрдд рдлрдХреНрдд "photo box 17 рдордзреНрдпреЗ" рдПрд╡рдвреЗрдЪ рд▓рд┐рд╣рд┐рд▓реЗ рдЬрд╛рддреЗ.

рджреАрдкрд┐рдХрд╛ рдкрд╣рд┐рд▓реНрдпрд╛рдЪ рджрд┐рд╡рд╢реА рдкрд╛рдЪ рдХрдкрд╛рдЯреЗ рдШреЗрдд рдирд╛рд╣реА. рдпреЛрдЧреНрдп рдЦреВрдг-рдкрдЯреНрдЯреНрдпрд╛рдВрдЪреА рдПрдХ рдЪрд╛рдВрдЧрд▓реА рдиреЛрдВрджрд╡рд╣реА рдЬрд╡рд│рдЬрд╡рд│ рд╕рдЧрд│реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдЪреА рдЙрддреНрддрд░реЗ рджреЗрддреЗ. рдПрдЦрд╛рджреНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рд▓рд╛ рдЧрд░рдЬ рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рддреА рдЦрд╛рд╕ рдХрдкрд╛рдЯ рдЬреЛрдбрддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart TB
    q["тЭУ access patterns first<br/>notices of class 3A, newest first<br/>one notice by id ┬╖ my children's feed"]
    q --> rel["ЁЯРШ relational (PostgreSQL / Aurora)<br/>the safe default<br/>joins ┬╖ transactions"]
    rel --> t["notices(id, class_id, author, title,<br/>text, urgent, created_at)<br/>index (class_id, created_at DESC)"]
    q -.->|"huge scale + key lookups"| kv["ЁЯФС key-value<br/>DynamoDB / Cassandra"]
    q -.->|"full-text search"| se["ЁЯФО search<br/>OpenSearch"]
    q -.->|"similar meaning"| vx["ЁЯзн vector<br/>pgvector / k-NN"]
    q -.->|"photos, files"| ob["ЁЯкг object storage<br/>S3"]
    q -.->|"metrics over time"| ts["тП▒я╕П time-series<br/>Timestream / Prometheus"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг data code рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдХрд╛рд│ рдЯрд┐рдХрддреЛ. рдЖрдЬ рд╡рд╛рдИрдЯ рд░рдЪрд▓реЗрд▓реЗ table рд╡рд░реНрд╖рд╛рдиреБрд╡рд░реНрд╖реЗ рдХрд╛рд│рдЬреАрдкреВрд░реНрд╡рдХ migrate рдХрд░рд╛рд╡реЗ рд▓рд╛рдЧреЗрд▓. рдЖрдгрд┐ рдХрд╛рд░рдг "рдХреЛрдгрддрд╛ database?" рд╣рд╛ рдкрд╣рд┐рд▓рд╛ рдкреНрд░рд╢реНрдирдЪ рдЪреБрдХреАрдЪрд╛ рдЖрд╣реЗ. рдпреЛрдЧреНрдп рдкрд╣рд┐рд▓рд╛ рдкреНрд░рд╢реНрди рдЖрд╣реЗ "рдХреЛрдгрддреЗ рдкреНрд░рд╢реНрди, рдХрд┐рддреА рд╡реЗрд│рд╛, рдХрд┐рддреА рдЬрд▓рдж?" тАФ store рддреНрдпрд╛рддреВрди рдард░рддреЛ. рдкреНрд░рддреНрдпреЗрдХ рдЬрд╛рд╕реНрддреАрдЪрд╛ store рдореНрд╣рдгрдЬреЗ рдЪрд╛рд▓рд╡рд╛рдпрд▓рд╛, backup рдШреНрдпрд╛рдпрд▓рд╛, рд╕реБрд░рдХреНрд╖рд┐рдд рдареЗрд╡рд╛рдпрд▓рд╛ рдЖрдгрд┐ рдЬреБрд│рд╡реВрди рдареЗрд╡рд╛рдпрд▓рд╛ рдЖрдгрдЦреА рдПрдХ system. 4 рдЬрдгрд╛рдВрдЪреНрдпрд╛ team рдиреЗ (рдзрдбрд╛ 01) рдПрдЦрд╛рджреНрдпрд╛ access pattern рд▓рд╛ рд╕реНрдкрд╖реНрдЯрдкрдгреЗ рджреБрд╕рд░рд╛ рд▓рд╛рдЧрдд рдирд╛рд╣реА рддреЛрдкрд░реНрдпрдВрдд рдПрдХрдЪ рдЪрд╛рд▓рд╡рд╛рд╡рд╛.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

design/designs.py рдордзреАрд▓ choose_store(needs) рдирд┐рдпрдорд╛рдВрдЪреА рдПрдХ рдпрд╛рджреА рддрдкрд╛рд╕рддреЗ тАФ {transactions, joins}, {key-lookup, huge-scale}, {search}, {similarity}, {files}, {time-series} тАФ рдЖрдгрд┐ рдЬреНрдпрд╛рдЪреЗ рд╕рдЧрд│реЗ рд╢рдмреНрдж needs рдордзреНрдпреЗ рдЖрд╣реЗрдд рдЕрд╕рд╛ рдкрд╣рд┐рд▓рд╛ рдирд┐рдпрдо рдкрд░рдд рджреЗрддреЗ. рдХреЛрдгрддрд╛рдЪ рдЬреБрд│рд▓рд╛ рдирд╛рд╣реА, рддрд░ рддреЗ relational рдкрд░рдд рджреЗрддреЗ: "access pattern рд╡реЗрдЧрд│реЗ рд╕рд╛рдВрдЧреЗрдкрд░реНрдпрдВрдд рд╕реБрд░рдХреНрд╖рд┐рдд default". рд╣рд╛ рдЪрд░реНрдЪрд╛ рд╕реБрд░реВ рдХрд░рдгреНрдпрд╛рд╕рд╛рдареАрдЪрд╛ рд╢рд┐рдХрд╡рдгреАрдЪрд╛ рдирд┐рдпрдо рдЖрд╣реЗ, рдХрд╛рдпрджрд╛ рдирд╛рд╣реА. design/demo.py рдордзреАрд▓ data() рдЧрд░рдЬрд╛рдВрдЪреЗ рд╕рд╣рд╛ рд╕рдВрдЪ рд╡рд╛рдкрд░реВрди рдкрд╛рд╣рддреЗ рдЖрдгрд┐ рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рдЪреЗ table рдЖрдгрд┐ index рдЫрд╛рдкрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 design/demo.py data
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import choose_store
for needs in ({"time-series"}, {"search"}, {"search", "transactions", "joins"}, {"key-lookup"}, {"key-lookup", "huge-scale"}, {"transactions"}):
    print(f"{', '.join(sorted(needs)):<30} тЖТ {choose_store(needs)}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

data рдЫрд╛рдкрддреЗ:

тФАтФА model the data from the questions you will ask it (access patterns), then pick the store
   joins, transactions          тЖТ relational (PostgreSQL / Aurora)
   huge-scale, key-lookup       тЖТ key-value / wide-column (DynamoDB / Cassandra)
   search                       тЖТ search engine (OpenSearch)
   similarity                   тЖТ vector index (pgvector / OpenSearch k-NN)
   files                        тЖТ object storage (S3)
   (nothing special)            тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise
   notice board: notices(id, class_id, author, title, text, urgent, created_at) ┬╖ index (class_id, created_at DESC)
   one table, one index, one question answered fast тАФ add stores only when a new access pattern needs one

рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:

time-series                    тЖТ time-series store (Timestream / Prometheus)
search                         тЖТ search engine (OpenSearch)
joins, search, transactions    тЖТ relational (PostgreSQL / Aurora)
key-lookup                     тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise
huge-scale, key-lookup         тЖТ key-value / wide-column (DynamoDB / Cassandra)
transactions                   тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдлрдХреНрдд "key lookups" рд╣реЗ key-value store рд╕рд╛рдареА рдкреБрд░реЗрд╕реЗ рдХрд╛рд░рдг рдирд╛рд╣реА тАФ PostgreSQL key lookups рдЪрд╛рдВрдЧрд▓реЗ рдХрд░рддреЛ. Scale рдЖрдгрд┐ key lookups рдорд┐рд│реВрди рддреЗ рдХрд╛рд░рдг рдмрдирддреЗ. рдЖрдгрд┐ app рд▓рд╛ joins рдЖрдгрд┐ search рджреЛрдиреНрд╣реА рд▓рд╛рдЧрдд рдЕрд╕рддреАрд▓, рддрд░ рдирд┐рдпрдо рдЖрдзреА relational рдирд┐рд╡рдбрддреЛ: рдореВрд│ рдиреЛрдВрджреАрдВрдЪреА system (system of record) relational рд░рд╛рд╣рддреЗ, рдЖрдгрд┐ search рддреНрдпрд╛рдЪреНрдпрд╛ рд╢реЗрдЬрд╛рд░реА рдПрдХ рдкреНрд░рдд рдореНрд╣рдгреВрди рдЬреЛрдбрд▓реЗ рдЬрд╛рддреЗ (events рдордзреВрди рднрд░рд▓реЗрд▓реЗ, рдзрдбрд╛ 08) тАФ рддреНрдпрд╛рдЪреНрдпрд╛ рдЬрд╛рдЧреА рдирд╛рд╣реА.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рдЪреЗ рдкрд╣рд┐рд▓реЗ table рдЖрдгрд┐ рддреНрдпрд╛рдЪрд╛ рдПрдХ рдЧрд░рдо index, PostgreSQL рдордзреНрдпреЗ:

CREATE TABLE notices (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  class_id    text        NOT NULL REFERENCES classes(id),
  author_id   bigint      NOT NULL REFERENCES teachers(id),
  title       text        NOT NULL,
  body        text        NOT NULL,                   -- "text" in the demo
  urgent      boolean     NOT NULL DEFAULT false,
  photo_key   text,                                   -- the S3 object key, not the photo
  created_at  timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX notices_class_newest ON notices (class_id, created_at DESC, id DESC);

EXPLAIN SELECT id, title FROM notices WHERE class_id = '3A' ORDER BY created_at DESC, id DESC LIMIT 20;
-- look for: Index Scan using notices_class_newest (not Seq Scan)

On a real account тАФ рдкреБрдвреЗ рдПрдЦрд╛рджреНрдпрд╛ access pattern рд▓рд╛ key-value scale рд▓рд╛рдЧрд▓рд╛ (рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде key рдиреЗ рд╡рд╛рдЪрд▓реНрдпрд╛ рдЬрд╛рдгрд╛рд▒реНрдпрд╛ 100 million рдкрд╛рд▓рдХ-inbox rows), рддрд░ рддреНрдпрд╛ рдПрдХрд╛ рдкреНрд░рд╢реНрдирд╛рд╕рд╛рдареА рд░рдЪрд▓реЗрд▓реЗ DynamoDB table:

aws dynamodb create-table --table-name parent-inbox \
    --attribute-definitions AttributeName=parent_id,AttributeType=S AttributeName=created_at,AttributeType=S \
    --key-schema AttributeName=parent_id,KeyType=HASH AttributeName=created_at,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

рдЕрдзрд┐рдХ рдЦреЛрд▓рд╛рдд: Database рд╢рд╛рд│рд╛ (normal forms, joins, indexes рдЖрдгрд┐ EXPLAIN, transactions, replicas, partitioning, NoSQL) рдЖрдгрд┐ Vector DB рд╢рд╛рд│рд╛.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: access patterns рдЪреА рдпрд╛рджреА design doc рдордзреНрдпреЗ schema рдЪреНрдпрд╛ рд╢реЗрдЬрд╛рд░реА рд▓рд┐рд╣рд╛. рдХреЛрдгреА рд╡рд┐рдЪрд╛рд░рд▓реЗ "DynamoDB рдХрд╛ рдирд╛рд╣реА?" рддрд░ рдЙрддреНрддрд░ рдореНрд╣рдгрдЬреЗ рддреНрдпрд╛ рдпрд╛рджреАрддрд▓реА рдПрдХ рдУрд│, рднрд╛рд╡рдирд╛ рдирд╡реНрд╣реЗ.

тПня╕П рдкреБрдвреЗ

рднрд╛рдЧ 1 рдкреВрд░реНрдг рдЭрд╛рд▓рд╛: рдХрд╛рдЧрдж, рдЖрдХрдбреЗ, рдХрд░рд╛рд░, data. рдЖрдХрдбреЗ рдореНрд╣рдгрд╛рд▓реЗ рдкреНрд░рддреНрдпреЗрдХ write рдорд╛рдЧреЗ 100 reads. рднрд╛рдЧ 2 рддреНрдпрд╛рд╕рд╛рдареАрдЪреНрдпрд╛ рдкрд╣рд┐рд▓реНрдпрд╛ рдмрд╛рдВрдзрдгреАрдЪреНрдпрд╛ рдареЛрдХрд│реНрдпрд╛рдиреЗ рд╕реБрд░реВ рд╣реЛрддреЛ: рд╡рд╛рдЪрдгрд╛рд▒реНрдпрд╛рдЬрд╡рд│ рдкреНрд░рддреА рдареЗрд╡рдгреЗ.

git checkout lesson-05-caching

ЁЯЧДя╕П Lesson 04 тАФ Data model & database choice: model the questions first

ЁЯУН You are here: Lesson 04 of 18 ┬╖ Previous: lesson-03-api-design ┬╖ Next: lesson-05-caching


ЁЯУж What's in this branch

Lessons 01тАУ03, plus the last step of Part 1: the data. List the access patterns (the questions the app will ask the data), write the table and the index that answers them, and only then pick the kind of store тАФ relational by default, and a key-value store, search engine, vector index, object storage or time-series store only when an access pattern needs one. choose_store() in design/designs.py turns needs into a store; data() in design/demo.py prints the notice board's choice.

ЁЯзТ Explain like I'm 5

The school office keeps a big register ЁЯУТ. Before Dipika buys a cupboard for it, she asks: "What will people look up in it?"

For the first question, she puts the pages in order by class, then by date, with a tab for each class ЁЯУС. Now the clerk opens the 3A tab and reads the top 20. That tab is an index.

For the "sports day" question, a normal register is bad тАФ the clerk would read every page. So if people really ask it a lot, Dipika adds a separate word index at the back of the book: "sports day тЖТ pages 12, 88, 301". That is a search engine.

Photos do not go in the register at all. They go in a store room ЁЯЧГя╕П, and the register only writes "photo in box 17".

Dipika does not buy five cupboards on day one. One good register with the right tabs answers almost everything. She adds a special cupboard only when a question needs it.

ЁЯЧ║я╕П Diagram

flowchart TB
    q["тЭУ access patterns first<br/>notices of class 3A, newest first<br/>one notice by id ┬╖ my children's feed"]
    q --> rel["ЁЯРШ relational (PostgreSQL / Aurora)<br/>the safe default<br/>joins ┬╖ transactions"]
    rel --> t["notices(id, class_id, author, title,<br/>text, urgent, created_at)<br/>index (class_id, created_at DESC)"]
    q -.->|"huge scale + key lookups"| kv["ЁЯФС key-value<br/>DynamoDB / Cassandra"]
    q -.->|"full-text search"| se["ЁЯФО search<br/>OpenSearch"]
    q -.->|"similar meaning"| vx["ЁЯзн vector<br/>pgvector / k-NN"]
    q -.->|"photos, files"| ob["ЁЯкг object storage<br/>S3"]
    q -.->|"metrics over time"| ts["тП▒я╕П time-series<br/>Timestream / Prometheus"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l04

тЭУ What

ЁЯдФ Why

Because the data outlives the code. A table you design badly today will be migrated, with care, for years. And because "which database?" is the wrong first question. The right first question is "which questions, how often, how fast?" тАФ the store follows from that. Every extra store is one more system to run, back up, secure and keep in step. A team of 4 (lesson 01) should run one unless an access pattern clearly needs a second.

ЁЯФз How (in this repo)

choose_store(needs) in design/designs.py walks a list of rules тАФ {transactions, joins}, {key-lookup, huge-scale}, {search}, {similarity}, {files}, {time-series} тАФ and returns the first rule whose words are all in needs. If none match, it returns relational: "the safe default until an access pattern says otherwise". It is a teaching rule to start a discussion, not a law. data() in design/demo.py tries six sets of needs and prints the notice board's table and index.

ЁЯзк Try it

python3 design/demo.py data
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import choose_store
for needs in ({"time-series"}, {"search"}, {"search", "transactions", "joins"}, {"key-lookup"}, {"key-lookup", "huge-scale"}, {"transactions"}):
    print(f"{', '.join(sorted(needs)):<30} тЖТ {choose_store(needs)}")
EOF

тЬЕ Verify тАФ what you should see

data prints:

тФАтФА model the data from the questions you will ask it (access patterns), then pick the store
   joins, transactions          тЖТ relational (PostgreSQL / Aurora)
   huge-scale, key-lookup       тЖТ key-value / wide-column (DynamoDB / Cassandra)
   search                       тЖТ search engine (OpenSearch)
   similarity                   тЖТ vector index (pgvector / OpenSearch k-NN)
   files                        тЖТ object storage (S3)
   (nothing special)            тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise
   notice board: notices(id, class_id, author, title, text, urgent, created_at) ┬╖ index (class_id, created_at DESC)
   one table, one index, one question answered fast тАФ add stores only when a new access pattern needs one

Your snippet prints:

time-series                    тЖТ time-series store (Timestream / Prometheus)
search                         тЖТ search engine (OpenSearch)
joins, search, transactions    тЖТ relational (PostgreSQL / Aurora)
key-lookup                     тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise
huge-scale, key-lookup         тЖТ key-value / wide-column (DynamoDB / Cassandra)
transactions                   тЖТ relational (PostgreSQL) тАФ the safe default until an access pattern says otherwise

ЁЯПБ What you just proved

"Key lookups" alone do not justify a key-value store тАФ PostgreSQL does key lookups well. Scale plus key lookups does. And when an app needs joins and search, the rule picks relational first: the system of record stays relational, and search is added next to it as a copy (fed by events, lesson 08) тАФ not instead of it.

тЪая╕П Common mistakes

ЁЯПн In production

The notice board's first table and its one hot index, in PostgreSQL:

CREATE TABLE notices (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  class_id    text        NOT NULL REFERENCES classes(id),
  author_id   bigint      NOT NULL REFERENCES teachers(id),
  title       text        NOT NULL,
  body        text        NOT NULL,                   -- "text" in the demo
  urgent      boolean     NOT NULL DEFAULT false,
  photo_key   text,                                   -- the S3 object key, not the photo
  created_at  timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX notices_class_newest ON notices (class_id, created_at DESC, id DESC);

EXPLAIN SELECT id, title FROM notices WHERE class_id = '3A' ORDER BY created_at DESC, id DESC LIMIT 20;
-- look for: Index Scan using notices_class_newest (not Seq Scan)

On a real account тАФ if one access pattern later needs key-value scale (for example 100 million per-parent inbox rows read by key), a DynamoDB table designed for that one question:

aws dynamodb create-table --table-name parent-inbox \
    --attribute-definitions AttributeName=parent_id,AttributeType=S AttributeName=created_at,AttributeType=S \
    --key-schema AttributeName=parent_id,KeyType=HASH AttributeName=created_at,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

Go deeper: the Database school (normal forms, joins, indexes and EXPLAIN, transactions, replicas, partitioning, NoSQL) and the Vector DB school.

ЁЯПн Why this matters in production: write the access-pattern list into the design doc next to the schema. When someone asks "why not DynamoDB?" the answer is a line in that list, not a feeling.

тПня╕П Next

Part 1 is done: the sheet, the numbers, the contract, the data. The numbers said 100 reads for every write. Part 2 starts with the first building block for that: keeping copies close to the reader.

git checkout lesson-05-caching
тЖР Previousapi designNext тЖТcaching

This page is the lesson's README from the lesson-04-data-model branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.