ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯзй рдзрдбрд╛ 09 тАФ Monolith vs microservices: modular рдиреЗ рд╕реБрд░реБрд╡рд╛рдд рдХрд░рд╛, рдХрд╛рд░рдг рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рд╡рд┐рднрд╛рдЧрд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзй рдзрдбрд╛ 09 тАФ Monolith vs microservices: modular рдиреЗ рд╕реБрд░реБрд╡рд╛рдд рдХрд░рд╛, рдХрд╛рд░рдг рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рд╡рд┐рднрд╛рдЧрд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 09 ┬╖ рдорд╛рдЧреАрд▓: lesson-08-events ┬╖ рдкреБрдвреАрд▓: lesson-10-consistency


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

рдзрдбреЗ 01тАУ08, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд░рдЪрдиреЗрд╕рдореЛрд░ рдпреЗрдгрд╛рд░рд╛ рдкреНрд░рд╢реНрди: рдПрдХ deploy рд╣реЛрдгрд╛рд░рд╛ program рдХреА рдЕрдиреЗрдХ services? рд╕реБрд░реБрд╡рд╛рдд modular monolith рдиреЗ рдХрд░рд╛. рдкреНрд░рддреНрдпреЗрдХ network hop latency рд╡рд╛рдврд╡рддреЛ рдЖрдгрд┐ availability рдЪрд╛ рдЧреБрдгрд╛рдХрд╛рд░ рдХрд░рддреЛ (0.999^n). Distributed monolith рдордзреНрдпреЗ рд╕рдЧрд│рд╛ рдЦрд░реНрдЪ рдЕрд╕рддреЛ рдЖрдгрд┐ рдлрд╛рдпрджрд╛ рдХрд╛рд╣реАрдЪ рдирд╕рддреЛ. Teams, scaling рдХрд┐рдВрд╡рд╛ failure isolation рд╕реНрдкрд╖реНрдЯрдкрдгреЗ рдорд╛рдЧрддреАрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рд╡рд┐рднрд╛рдЧрд╛ тАФ рдЖрдгрд┐ Conway's law рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рд╛. design/designs.py рдордзреАрд▓ call_chain() рдЧрдгрд┐рдд рдХрд░рддреЛ; design/demo.py рдордзреАрд▓ services() рддреЗ рдЫрд╛рдкрддреЛ.

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

рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдд рдЪрд╛рд░ рдЯреЗрдмрд▓рд╛рдВрдЪреА рдПрдХ рдЦреЛрд▓реА рдЖрд╣реЗ: рд╕реВрдЪрдирд╛, рдкрд╛рд▓рдХ, SMS рдЖрдгрд┐ рдЕрд╣рд╡рд╛рд▓. рдкреНрд░рддреНрдпреЗрдХ рдЯреЗрдмрд▓рд╛рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ рдбреНрд░реЙрд╡рд░ рдЖрдгрд┐ рд╕реНрд╡рддрдГрдЪрд╛ рдХрд╛рд░рдХреВрди рдЖрд╣реЗ, рдЖрдгрд┐ рддреЗ рдХрд╛рдЧрдж рдЦреЛрд▓реАрддрд▓реНрдпрд╛ рдЦреЛрд▓реАрдд рд╣рд╛рддрд╛рдиреЗ рджреЗрддрд╛рдд. рддреНрдпрд╛рд▓рд╛ рдПрдХ рд╕реЗрдХрдВрдж рд▓рд╛рдЧрддреЛ. рдХрд╛рд░реНрдпрд╛рд▓рдп рд╣рд▓рд▓реЗ рдХреА рдкреВрд░реНрдг рдЦреЛрд▓реА рдПрдХрддреНрд░ рд╣рд▓рддреЗ. рд╣реЗрдЪ modular monolith: рдПрдХ рдЗрдорд╛рд░рдд, рд╕реНрдкрд╖реНрдЯ рдЯреЗрдмрд▓реЗ.

рдХреЛрдгреАрддрд░реА рдореНрд╣рдгрддреЗ: "рдкреНрд░рддреНрдпреЗрдХ рдЯреЗрдмрд▓ рд╢рд╣рд░рд╛рдд рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдЗрдорд╛рд░рддреАрдд рдареЗрд╡реВ тАФ рддреЗ рдЬрд╛рд╕реНрдд рдЖрдзреБрдирд┐рдХ рдЖрд╣реЗ." рдЖрддрд╛ рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рдЧрдж рд╕рд╛рдпрдХрд▓рдиреЗ ЁЯЪ▓ рдЬрд╛рддреЛ. рдкреНрд░рддреНрдпреЗрдХ рдлреЗрд░реАрд▓рд╛ 20 рдорд┐рдирд┐рдЯреЗ рд▓рд╛рдЧрддрд╛рдд, рдЖрдгрд┐ рдХрд╛рд╣реА рджрд┐рд╡рд╢реА рд╕рд╛рдпрдХрд▓ рдмрд┐рдШрдбрддреЗ. рдЪрд╛рд░ рдЯреЗрдмрд▓рд╛рдВрдЪреА рдЧрд░рдЬ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдкрд╛рд▓рдХрд╛рдЪреНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рд▓рд╛ рдЖрддрд╛ рдЪрд╛рд░ рд╕рд╛рдпрдХрд▓ рдлреЗрд▒реНрдпрд╛ рд▓рд╛рдЧрддрд╛рдд, рдЖрдгрд┐ рдХреЛрдгрддреАрд╣реА рд╕рд╛рдпрдХрд▓ рдмрд┐рдШрдбрд▓реА рддрд░ рдкрд╛рд▓рдХрд╛рд▓рд╛ рдЙрддреНрддрд░ рдорд┐рд│рдд рдирд╛рд╣реА.

рд╡реЗрдЧрд│реА рдЗрдорд╛рд░рдд рдХрдзреА рдлрд╛рдпрджреНрдпрд╛рдЪреА? рдЬреЗрд╡реНрд╣рд╛ рдПрдЦрд╛рджреНрдпрд╛ рдЯреЗрдмрд▓рд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА team рдЕрд╕рддреЗ рдЬрд┐рд▓рд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рд╡реЗрд│реЗрдд рдХрд╛рдо рдХрд░рд╛рдпрдЪреЗ рдЕрд╕рддреЗ, рдЬреЗрд╡реНрд╣рд╛ рдПрдХрд╛ рдЯреЗрдмрд▓рд╛рд▓рд╛ рдмрд╛рдХреАрдЪреНрдпрд╛рдВрдкреЗрдХреНрд╖рд╛ рджрд╣рд╛рдкрдЯ рдЬрд╛рд╕реНрдд рдЬрд╛рдЧрд╛ рд▓рд╛рдЧрддреЗ, рдХрд┐рдВрд╡рд╛ рдЬреЗрд╡реНрд╣рд╛ рддреНрдпрд╛ рдЯреЗрдмрд▓рд╛рд▓рд╛ рд▓рд╛рдЧрд▓реЗрд▓реНрдпрд╛ рдЖрдЧреАрдиреЗ рдкреВрд░реНрдг рдХрд╛рд░реНрдпрд╛рд▓рдп рдмрдВрдж рд╣реЛрддрд╛ рдХрд╛рдорд╛ рдирдпреЗ. рддреНрдпрд╛рдЖрдзреА рдирд╛рд╣реА.

рдЖрдгрд┐ рджреАрдкрд┐рдХрд╛рдЪрд╛ рдПрдХ рдЗрд╢рд╛рд░рд╛: рдЪрд╛рд░рд╣реА рдЗрдорд╛рд░рддреАрдВрдирд╛ рддрд░реАрд╣реА рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдПрдХрд╛рдЪ рджрд┐рд╡рд╢реА рд╣рд▓рд╛рд╡реЗ рд▓рд╛рдЧрдд рдЕрд╕реЗрд▓, рддрд░ рддреБрдореНрд╣рд╛рд▓рд╛ рд╡реЗрдЧрд│реНрдпрд╛ рдЗрдорд╛рд░рддреА рдорд┐рд│рд╛рд▓реНрдпрд╛рдЪ рдирд╛рд╣реАрдд. рддреБрдореНрд╣рд╛рд▓рд╛ рдордзреЛрдордз рд╕рд╛рдпрдХрд▓реА рдЕрд╕рд▓реЗрд▓реЗ рдПрдХрдЪ рдХрд╛рд░реНрдпрд╛рд▓рдп рдорд┐рд│рд╛рд▓реЗ.

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

flowchart LR
    subgraph mono["ЁЯПв modular monolith тАФ one deploy"]
      n["notices"] --- p["parents"]
      p --- s["sms"]
      s --- r["reports"]
    end
    subgraph micro["ЁЯПЩя╕П microservices тАФ a network hop per call"]
      g["gateway"] -->|"20 ms ┬╖ 99.9%"| a["svc 1"]
      a -->|"20 ms ┬╖ 99.9%"| b["svc 2"]
      b -->|"20 ms ┬╖ 99.9%"| c["svc тАж 8"]
    end
    m["1 hop тЖТ 20 ms ┬╖ 99.90%<br/>3 hops тЖТ 60 ms ┬╖ 99.70%<br/>8 hops тЖТ 160 ms ┬╖ 99.20%"]
    micro --> m

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдХрд╛рдЧрдж (рдзрдбрд╛ 01) рд╕рд╛рдВрдЧрддреЛ 4 рдЬрдгрд╛рдВрдЪреА team, 3 рдорд╣рд┐рдиреНрдпрд╛рдВрдд launch, 99.9% available, p99 < 200 ms. рдмрд╛рд░рд╛ services latency budget рдЦрд╛рдКрди рдЯрд╛рдХрддреАрд▓, availability рдЙрджреНрджрд┐рд╖реНрдЯрд╛рдЪреНрдпрд╛ рдЦрд╛рд▓реА рдврдХрд▓рддреАрд▓, рдЖрдгрд┐ рдЪрд╛рд░ рд▓реЛрдХрд╛рдВрдирд╛ рдмрд╛рд░рд╛ pipelines, рдмрд╛рд░рд╛ dashboards рдЖрдгрд┐ рдЪрд╛рд▓рд╡рд╛рдпрд▓рд╛ рдПрдХ platform рджреЗрддреАрд▓. Modular monolith рдкреНрд░рддреНрдпреЗрдХ рдЖрдХрдбрд╛ рдкреВрд░реНрдг рдХрд░рддреЛ. рдкрдг рдПрдХрд╛ рднрд╛рдЧрд╛рдХрдбреЗ рдЖрдзреАрдЪ рд╡реЗрдЧрд│реЗ рд░рд╛рд╣рдгреНрдпрд╛рдЪреЗ рдХрд╛рд░рдг рдЖрд╣реЗ: SMS workers (рдзрдбрд╛ 07) рд╕реНрд╡рддрдВрддреНрд░рдкрдгреЗ scale рд╣реЛрддрд╛рдд рдЖрдгрд┐ рддреНрдпрд╛рдВрдиреА API рдордВрдж рдХрд░рддрд╛ рдХрд╛рдорд╛ рдирдпреЗ. рд╣реЗ рдХрд╛рд░рдгрд╛рд╕рд╣рд┐рдд рд╡рд┐рднрд╛рдЧрдгреЗ рдЖрд╣реЗ.

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

design/designs.py рдордзреАрд▓ call_chain(hops, ms_each, availability_each) latency_ms = hops ├Ч ms_each рдЖрдгрд┐ availability = availability_each ** hops рдкрд░рдд рдХрд░рддреЛ тАФ рдПрдХрд╛ рдУрд│реАрдд call рдХреЗрд▓реЗрд▓реНрдпрд╛ services, рдкреНрд░рддреНрдпреЗрдХ рдЖрд╡рд╢реНрдпрдХ. design/demo.py рдордзреАрд▓ services() 20 ms рдЖрдгрд┐ 99.9% рд▓рд╛ 1, 3 рдЖрдгрд┐ 8 hops рдЫрд╛рдкрддреЛ. Snippet 99.99% services рдкрдг рд╡рд╛рдкрд░реВрди рдкрд╛рд╣рддреЛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ availability 30 рджрд┐рд╡рд╕рд╛рдВрддреАрд▓ рдмрдВрдж рдорд┐рдирд┐рдЯрд╛рдВрдд рдмрджрд▓рддреЛ.

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

python3 design/demo.py services
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import call_chain
for a in (0.999, 0.9999):
    for hops in (1, 5, 10, 20):
        c = call_chain(hops, 20, a)
        print(f"each {a:.2%} ┬╖ {hops:>2} hops тЖТ {c['latency_ms']:>3} ms ┬╖ {c['availability']:.3%} ┬╖ down {(1 - c['availability']) * 30 * 24 * 60:6.1f} min / 30 days")
EOF
python3 design/test_design.py

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

services рд╣реЗ рдЫрд╛рдкрддреЛ:

тФАтФА one request that calls services in a line (each 20 ms, each 99.9% available)
   1 hops тЖТ  20 ms, availability 99.90%
   3 hops тЖТ  60 ms, availability 99.70%
   8 hops тЖТ 160 ms, availability 99.20%
   start as a MODULAR MONOLITH: one deploy, clear modules; split a service out when a team or a scaling need asks
   a 'distributed monolith' = many services that must deploy together тАФ all the cost, none of the benefit

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

each 99.90% ┬╖  1 hops тЖТ  20 ms ┬╖ 99.900% ┬╖ down   43.2 min / 30 days
each 99.90% ┬╖  5 hops тЖТ 100 ms ┬╖ 99.501% ┬╖ down  215.6 min / 30 days
each 99.90% ┬╖ 10 hops тЖТ 200 ms ┬╖ 99.004% ┬╖ down  430.1 min / 30 days
each 99.90% ┬╖ 20 hops тЖТ 400 ms ┬╖ 98.019% ┬╖ down  855.8 min / 30 days
each 99.99% ┬╖  1 hops тЖТ  20 ms ┬╖ 99.990% ┬╖ down    4.3 min / 30 days
each 99.99% ┬╖  5 hops тЖТ 100 ms ┬╖ 99.950% ┬╖ down   21.6 min / 30 days
each 99.99% ┬╖ 10 hops тЖТ 200 ms ┬╖ 99.900% ┬╖ down   43.2 min / 30 days
each 99.99% ┬╖ 20 hops тЖТ 400 ms ┬╖ 99.800% ┬╖ down   86.3 min / 30 days

Tests рд╢реЗрд╡рдЯреА 18/18 passed рджрд╛рдЦрд╡рддрд╛рдд.

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

рдУрд│реАрддреАрд▓ 99.9% рдЪреНрдпрд╛ рджрд╣рд╛ services рдПрдХрд╛ service рдкреЗрдХреНрд╖рд╛ рд╕реБрдорд╛рд░реЗ рджрд╣рд╛рдкрдЯ рдЬрд╛рд╕реНрдд рд╡реЗрд│ рдмрдВрдж рдЕрд╕рддрд╛рдд (рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ 430 vs 43 рдорд┐рдирд┐рдЯреЗ), рдЖрдгрд┐ рддреНрдпрд╛ рд╕рдВрдкреВрд░реНрдг 200 ms read budget рдлрдХреНрдд hops рд╡рд░рдЪ рдЦрд░реНрдЪ рдХрд░рддрд╛рдд. 10-hop request 99.9% рд╡рд░ рдареЗрд╡рд╛рдпрдЪрд╛ рддрд░ рдкреНрд░рддреНрдпреЗрдХ service рд▓рд╛ 99.99% рдЧрд╛рдард╛рд╡реЗ рд▓рд╛рдЧреЗрд▓ тАФ рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рджрд╣рд╛рдкрдЯ рдХрдареАрдг. Hops рдлреБрдХрдЯ рдирд╕рддрд╛рдд; рдХрд╛рд░рдг рдХрд┐рдорддреАрдЗрддрдХреЗ рдореЛрд▓рд╛рдЪреЗ рдЕрд╕реЗрд▓ рддрд┐рдереЗрдЪ рд╡рд┐рднрд╛рдЧрд╛.

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

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

Modular monolith рдореНрд╣рдгрдЬреЗ рдПрдХ folder рд░рдЪрдирд╛ рдЖрдгрд┐ рддрдкрд╛рд╕рд▓рд╛ рдЬрд╛рдгрд╛рд░рд╛ рдПрдХ рдирд┐рдпрдо. Python рдордзреНрдпреЗ рд╕реВрдЪрдирд╛ рдлрд▓рдХ:

notice_board/
тФЬтФАтФА notices/      api.py (public) ┬╖ _db.py ┬╖ _models.py    # owns tables: notices, outbox
тФЬтФАтФА parents/      api.py (public) ┬╖ _db.py                 # owns tables: parents, children
тФЬтФАтФА sms/          api.py (public) ┬╖ worker.py              # owns table: sms_sent тАФ the first to split out
тФФтФАтФА reports/      api.py (public)

рдирд┐рдпрдо, CI рдордзреНрдпреЗ import-linter рдиреЗ рддрдкрд╛рд╕рд▓реЗрд▓рд╛ (modules рдлрдХреНрдд рдПрдХрдореЗрдХрд╛рдВрдЪреНрдпрд╛ api рдордзреВрдирдЪ рдмреЛрд▓рддрд╛рдд):

# .importlinter
[importlinter]
root_package = notice_board

[importlinter:contract:modules]
name = modules are independent except through api
type = independence
modules =
    notice_board.notices
    notice_board.parents
    notice_board.sms
    notice_board.reports
ignore_imports =
    notice_board.* -> notice_board.*.api

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ SMS module рдХрдбреЗ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ deploy рдЪреЗ рд╕реНрдкрд╖реНрдЯ рдХрд╛рд░рдг рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛, рддреЛ рдзрдбрд╛ 07 рдордзреАрд▓ queue рд╡рд╛рдЪрдгрд╛рд░реА рд╕реНрд╡рддрдГрдЪреА ECS service (рдХрд┐рдВрд╡рд╛ Lambda) рдмрдирддреЛ, рд╕реНрд╡рддрдВрддреНрд░рдкрдгреЗ scale рд╣реЛрдгрд╛рд░реА:

aws ecs create-service --cluster notice-board --service-name sms-worker \
    --task-definition sms-worker:1 --desired-count 2 --launch-type FARGATE \
    --network-configuration 'awsvpcConfiguration={subnets=[subnet-0a1b2c3d],securityGroups=[sg-0123abcd]}'

рдЕрдзрд┐рдХ рдЦреЛрд▓рд╛рдд: Kubernetes рд╢рд╛рд│рд╛ рдЕрдиреЗрдХ services рдЪрд╛рд▓рд╡рддреЗ; API Gateway рд╢рд╛рд│рд╛ рддреНрдпрд╛рдВрдЪреНрдпрд╛рд╕рдореЛрд░ рдПрдХ рдореБрдЦреНрдп рджрд░рд╡рд╛рдЬрд╛ рдареЗрд╡рддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ service рдХрд╛ рдЖрд╣реЗ рддреЗ ADR (рдзрдбрд╛ 16) рдореНрд╣рдгреВрди рд▓рд┐рд╣реВрди рдареЗрд╡рд╛: "sms-worker рд╡реЗрдЧрд│реА рдЖрд╣реЗ рдХрд╛рд░рдг рддреА 200 workers рдкрд░реНрдпрдВрдд scale рд╣реЛрддреЗ рдЖрдгрд┐ рддрд┐рдЪреНрдпрд╛ provider рдЪреЗ outages posting рд╡рд░ рдкрд░рд┐рдгрд╛рдо рдХрд░рддрд╛ рдХрд╛рдорд╛ рдирдпреЗрдд." рд▓рд┐рдЦрд┐рдд рдХрд╛рд░рдг рдирд╕рд▓реЗрд▓реА service рдкрд░рдд рдЬреЛрдбрдгреНрдпрд╛рд╕рд╛рдареА рдЙрдореЗрджрд╡рд╛рд░ рдЕрд╕рддреЗ.

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

Services рдЖрдгрд┐ replicas рдореНрд╣рдгрдЬреЗ data рдЪреНрдпрд╛ рдкреНрд░рддреА рдПрдХрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдард┐рдХрд╛рдгреА. рд╢рд┐рдХреНрд╖рд┐рдХреЗрдиреЗ рд▓рд┐рд╣рд┐рд▓реНрдпрд╛рд╡рд░ рд▓рдЧреЗрдЪ рдкрд╛рд▓рдХрд╛рдиреЗ рд╡рд╛рдЪрд▓реЗ, рддрд░ рддрд┐рд▓рд╛ рдХреЛрдгрддреА рдкреНрд░рдд рджрд┐рд╕рддреЗ? рдирд┐рдпреЛрдЬрди рдХрд╛рд░реНрдпрд╛рд▓рдп рдард░рд╡рддреЗ рдХреА рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рдд рдХрд┐рддреА consistent рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реА.

git checkout lesson-10-consistency

ЁЯзй Lesson 09 тАФ Monolith vs microservices: start modular, split for a reason

ЁЯУН You are here: Lesson 09 of 18 ┬╖ Previous: lesson-08-events ┬╖ Next: lesson-10-consistency


ЁЯУж What's in this branch

Lessons 01тАУ08, plus the question every design meets: one deployable program or many services? Start as a modular monolith. Every network hop adds latency and multiplies availability (0.999^n). A distributed monolith has all the cost and none of the benefit. Split when teams, scaling or failure isolation clearly ask for it тАФ and remember Conway's law. call_chain() in design/designs.py does the arithmetic; services() in design/demo.py prints it.

ЁЯзТ Explain like I'm 5

The school office has one room with four desks: notices, parents, SMS and reports. Each desk has its own drawers and its own clerk, and they pass papers across the room by hand. It takes one second. When the office moves, the whole room moves together. That is a modular monolith: one building, clear desks.

Someone says: "Let's put each desk in its own building across town тАФ it is more modern." Now every paper goes by bicycle ЁЯЪ▓. Each trip takes 20 minutes, and some days a bicycle breaks. A parent's question that needs four desks now needs four bicycle trips, and if any bicycle breaks, the parent gets no answer.

When is a separate building worth it? When a desk has its own team that wants to work on its own hours, when one desk needs ten times more space than the rest, or when a fire at that desk must not close the whole office. Not before.

And a warning from Dipika: if the four buildings must still all move on the same day every time, you did not get separate buildings. You got one office with bicycles in the middle.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph mono["ЁЯПв modular monolith тАФ one deploy"]
      n["notices"] --- p["parents"]
      p --- s["sms"]
      s --- r["reports"]
    end
    subgraph micro["ЁЯПЩя╕П microservices тАФ a network hop per call"]
      g["gateway"] -->|"20 ms ┬╖ 99.9%"| a["svc 1"]
      a -->|"20 ms ┬╖ 99.9%"| b["svc 2"]
      b -->|"20 ms ┬╖ 99.9%"| c["svc тАж 8"]
    end
    m["1 hop тЖТ 20 ms ┬╖ 99.90%<br/>3 hops тЖТ 60 ms ┬╖ 99.70%<br/>8 hops тЖТ 160 ms ┬╖ 99.20%"]
    micro --> m

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

тЭУ What

ЁЯдФ Why

Because the sheet (lesson 01) says team of 4, launch in 3 months, 99.9% available, p99 < 200 ms. Twelve services would eat the latency budget, push availability below the target, and give four people twelve pipelines, twelve dashboards and a platform to run. A modular monolith meets every number. But one piece already has a reason to live apart: the SMS workers (lesson 07) scale on their own and must not slow the API. That is a split with a reason.

ЁЯФз How (in this repo)

call_chain(hops, ms_each, availability_each) in design/designs.py returns latency_ms = hops ├Ч ms_each and availability = availability_each ** hops тАФ services called in a line, each one needed. services() in design/demo.py prints 1, 3 and 8 hops at 20 ms and 99.9%. The snippet also tries 99.99% services and turns each availability into minutes down per 30 days.

ЁЯзк Try it

python3 design/demo.py services
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import call_chain
for a in (0.999, 0.9999):
    for hops in (1, 5, 10, 20):
        c = call_chain(hops, 20, a)
        print(f"each {a:.2%} ┬╖ {hops:>2} hops тЖТ {c['latency_ms']:>3} ms ┬╖ {c['availability']:.3%} ┬╖ down {(1 - c['availability']) * 30 * 24 * 60:6.1f} min / 30 days")
EOF
python3 design/test_design.py

тЬЕ Verify тАФ what you should see

services prints:

тФАтФА one request that calls services in a line (each 20 ms, each 99.9% available)
   1 hops тЖТ  20 ms, availability 99.90%
   3 hops тЖТ  60 ms, availability 99.70%
   8 hops тЖТ 160 ms, availability 99.20%
   start as a MODULAR MONOLITH: one deploy, clear modules; split a service out when a team or a scaling need asks
   a 'distributed monolith' = many services that must deploy together тАФ all the cost, none of the benefit

Your snippet prints:

each 99.90% ┬╖  1 hops тЖТ  20 ms ┬╖ 99.900% ┬╖ down   43.2 min / 30 days
each 99.90% ┬╖  5 hops тЖТ 100 ms ┬╖ 99.501% ┬╖ down  215.6 min / 30 days
each 99.90% ┬╖ 10 hops тЖТ 200 ms ┬╖ 99.004% ┬╖ down  430.1 min / 30 days
each 99.90% ┬╖ 20 hops тЖТ 400 ms ┬╖ 98.019% ┬╖ down  855.8 min / 30 days
each 99.99% ┬╖  1 hops тЖТ  20 ms ┬╖ 99.990% ┬╖ down    4.3 min / 30 days
each 99.99% ┬╖  5 hops тЖТ 100 ms ┬╖ 99.950% ┬╖ down   21.6 min / 30 days
each 99.99% ┬╖ 10 hops тЖТ 200 ms ┬╖ 99.900% ┬╖ down   43.2 min / 30 days
each 99.99% ┬╖ 20 hops тЖТ 400 ms ┬╖ 99.800% ┬╖ down   86.3 min / 30 days

The tests end with 18/18 passed.

ЁЯПБ What you just proved

Ten 99.9% services in a line are down about ten times as long as one (430 vs 43 minutes a month), and they spend the whole 200 ms read budget on hops alone. To keep a 10-hop request at 99.9%, every service must reach 99.99% тАФ ten times harder each. Hops are not free; split only where the reason is worth the price.

тЪая╕П Common mistakes

ЁЯПн In production

A modular monolith is a folder structure plus a rule that is checked. The notice board in Python:

notice_board/
тФЬтФАтФА notices/      api.py (public) ┬╖ _db.py ┬╖ _models.py    # owns tables: notices, outbox
тФЬтФАтФА parents/      api.py (public) ┬╖ _db.py                 # owns tables: parents, children
тФЬтФАтФА sms/          api.py (public) ┬╖ worker.py              # owns table: sms_sent тАФ the first to split out
тФФтФАтФА reports/      api.py (public)

The rule, checked in CI with import-linter (modules talk only through each other's api):

# .importlinter
[importlinter]
root_package = notice_board

[importlinter:contract:modules]
name = modules are independent except through api
type = independence
modules =
    notice_board.notices
    notice_board.parents
    notice_board.sms
    notice_board.reports
ignore_imports =
    notice_board.* -> notice_board.*.api

On a real account тАФ when the SMS module has a clear reason for its own deploy, it becomes its own ECS service (or Lambda) reading the queue from lesson 07, scaled on its own:

aws ecs create-service --cluster notice-board --service-name sms-worker \
    --task-definition sms-worker:1 --desired-count 2 --launch-type FARGATE \
    --network-configuration 'awsvpcConfiguration={subnets=[subnet-0a1b2c3d],securityGroups=[sg-0123abcd]}'

Go deeper: the Kubernetes school runs many services; the API Gateway school puts one front door in front of them.

ЁЯПн Why this matters in production: write down why each service exists, as an ADR (lesson 16): "sms-worker is separate because it scales to 200 workers and its provider's outages must not affect posting." A service with no written reason is a candidate to merge back.

тПня╕П Next

Services and replicas mean copies of data in more than one place. When a parent reads right after a teacher writes, which copy does she see? The planning office decides how consistent each copy must be.

git checkout lesson-10-consistency
тЖР PreviouseventsNext тЖТconsistency

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