ЁЯПл The SchoolтА║ЁЯПЫя╕П Software ArchitectureтА║ЁЯУМ рдзрдбрд╛ 07 тАФ Event-driven architecture рдЖрдгрд┐ CQRS: рд╕реВрдЪрдирд╛ рдлрд▓рдХ рдЖрдгрд┐ display board
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУМ рдзрдбрд╛ 07 тАФ Event-driven architecture рдЖрдгрд┐ CQRS: рд╕реВрдЪрдирд╛ рдлрд▓рдХ рдЖрдгрд┐ display board

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 07 ┬╖ рдорд╛рдЧрдЪрд╛: lesson-06-styles ┬╖ рдкреБрдврдЪрд╛: lesson-08-data-ownership


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

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ рдПрдХ рд╡реЗрдЧрд│реНрдпрд╛ рдкреНрд░рдХрд╛рд░рдЪреА рдорд╛рд░реНрдЧрд┐рдХрд╛. Admissions рдЖрддрд╛ grades, fees рдЖрдгрд┐ timetable рд▓рд╛ call рдХрд░рдд рдирд╛рд╣реА; рддреЗ рдПрдХ event publish рдХрд░рддреЗ тАФ "рдПрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рджрд╛рдЦрд▓ рдЭрд╛рд▓реА" тАФ рдЖрдгрд┐ рдЬреНрдпрд╛рд▓рд╛ рддреНрдпрд╛рдд рд░рд╕ рдЖрд╣реЗ рддреЛ subscribe рдХрд░рддреЛ. рдордЧ CQRS: рдЬреНрдпрд╛ model рдордзреНрдпреЗ рддреБрдореНрд╣реА рд▓рд┐рд╣рд┐рддрд╛ рддреЗ, рдЬреНрдпрд╛ model рдордзреВрди рддреБрдореНрд╣реА рд╡рд╛рдЪрддрд╛ рддреНрдпрд╛рдкрд╛рд╕реВрди рд╡реЗрдЧрд│реЗ рдареЗрд╡рд╛, рдЖрдгрд┐ рд╡рд╛рдЪрдирд╛рдЪреА рдмрд╛рдЬреВ рдереЛрдбреА рдорд╛рдЧреЗ рдЕрд╕рддреЗ рд╣реЗ рд╕реНрд╡реАрдХрд╛рд░рд╛. EventBus рдЖрдгрд┐ ReportCardProjection arch/models.py рдордзреНрдпреЗ, events() arch/demo.py рдордзреНрдпреЗ. System Design рд╢рд╛рд│реЗрдЪрд╛ events, pub/sub рдЖрдгрд┐ outbox рд╡рд░рдЪрд╛ рдзрдбрд╛ delivery рдЖрдгрд┐ outbox рд╢рд┐рдХрд╡рддреЛ; рд╣рд╛ рдзрдбрд╛ events рдореБрд│реЗ coupling рдЪреЗ рдХрд╛рдп рд╣реЛрддреЗ рдпрд╛рдмрджреНрджрд▓ рдЖрд╣реЗ.

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

рд╢рд╛рд│реЗрдд рдирд╡реАрди рдореВрд▓ рдЖрд▓реЗ рдХреА admissions office рдЖрдзреА рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧрд╛рдХрдбреЗ рдЪрд╛рд▓рдд рдЬрд╛рдпрдЪреЗ, рдордЧ рд╣рд┐рд╢реЛрдм рд╡рд┐рднрд╛рдЧрд╛рдХрдбреЗ, рдордЧ timetable office рдХрдбреЗ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХрд╛рд▓рд╛ рд╕рд╛рдВрдЧрд╛рдпрдЪреЗ. ЁЯЪ╢тАНтЩАя╕П рдирд╡реАрди рд╡рд╛рдЪрдирд╛рд▓рдп рд╕реБрд░реВ рдЭрд╛рд▓реЗ рддрд░ admissions рд▓рд╛ рдЪреМрдереА рдлреЗрд░реА рд╢рд┐рдХрд╛рд╡реА рд▓рд╛рдЧрд╛рдпрдЪреА.

рдЖрддрд╛ рд╕рднрд╛рдЧреГрд╣рд╛рдд рдПрдХ рд╕реВрдЪрдирд╛ рдлрд▓рдХ ЁЯУМ рдЖрд╣реЗ. Admissions рдПрдХрдЪ рд╕реВрдЪрдирд╛ рд▓рд╛рд╡рддреЗ: "рджреАрдкрд┐рдХрд╛ 5A рд╡рд░реНрдЧрд╛рдд рджрд╛рдЦрд▓ рдЭрд╛рд▓реА." рдПрд╡рдвреЗрдЪ. рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧ, рд╣рд┐рд╢реЛрдм рд╡рд┐рднрд╛рдЧ рдЖрдгрд┐ timetable office рдкреНрд░рддреНрдпреЗрдХрдЬрдг рдлрд▓рдХ рд╡рд╛рдЪрддреЛ рдЖрдгрд┐ рдЖрдкрд▓реЗ рдХрд╛рдо рдХрд░рддреЛ. рд╡рд╛рдЪрдирд╛рд▓рдп рд╕реБрд░реВ рдЭрд╛рд▓реЗ рдХреА рдЧреНрд░рдВрдердкрд╛рд▓ рдлрдХреНрдд рдлрд▓рдХ рд╡рд╛рдЪрд╛рдпрд▓рд╛ рд▓рд╛рдЧрддрд╛рдд тАФ admissions рдордзреНрдпреЗ рдХрд╛рд╣реАрдЪ рдмрджрд▓ рд╣реЛрдд рдирд╛рд╣реА.

рдЖрдгрд┐ рдЧреБрдг? рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рдЧреБрдг рдиреЛрдВрджрд╡рд╣реАрдд рд▓рд┐рд╣рд┐рддреЗ (рд▓рд┐рдЦрд╛рдг). рд╕рднрд╛рдЧреГрд╣рд╛рддреАрд▓ display board рдкреНрд░рддреНрдпреЗрдХ рдореБрд▓рд╛рдЪреА рд╕рд░рд╛рд╕рд░реА рджрд╛рдЦрд╡рддреЛ (рд╡рд╛рдЪрди). рдХреЛрдгреАрддрд░реА display board рдереЛрдбреНрдпрд╛ рд╡реЗрд│рд╛рдиреЗ рдЕрджреНрдпрдпрд╛рд╡рдд рдХрд░рддреЗ. рдХрд╛рд╣реА рдорд┐рдирд┐рдЯреЗ рдлрд▓рдХрд╛рд╡рд░ рдЬреБрдиреА рд╕рд░рд╛рд╕рд░реА рджрд┐рд╕реВ рд╢рдХрддреЗ. рдЬрд▓рдж, рд╕рд╛рдзреНрдпрд╛ рдлрд▓рдХрд╛рдЪреА рд╣реА рдХрд┐рдВрдордд рдЖрд╣реЗ.

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

flowchart LR
    adm["ЁЯЧВя╕П admissions"] -->|"publish StudentAdmitted(Dipika, 5A)"| board["ЁЯУМ notice board<br/>(event bus)"]
    board --> gr["ЁЯУЭ grades: opens a mark sheet"]
    board --> fee["ЁЯТ░ fees: opens an account"]
    board --> tt["ЁЯЧУя╕П timetable: assigns a class"]
    board -.-> lib["ЁЯУЦ library (new): no change to admissions"]
    subgraph cqrs["CQRS"]
      w["тЬНя╕П write side: marks 72, 85, 90"] -->|"MarkRecorded events"| proj["ЁЯФД projection"]
      proj --> r["ЁЯУ║ read side: average 78.5 тЖТ 82.33"]
    end

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдкреНрд░рддреНрдпреЗрдХ рдереЗрдЯ call рд╣реА call рдХрд░рдгрд╛рд▒реНрдпрд╛рдХрдбреВрди call рд╣реЛрдгрд╛рд▒реНрдпрд╛рдХрдбреЗ рдЬрд╛рдгрд╛рд░реА рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЖрд╣реЗ тАФ рддреАрди calls рдЕрд╕рд▓реЗрд▓реНрдпрд╛ admissions рдХрдбреЗ рддреАрди рдмрд╛рд╣реЗрд░ рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЖрд╣реЗрдд, рдЖрдгрд┐ рдЪреМрдерд╛ рд╡рд┐рднрд╛рдЧ рдореНрд╣рдгрдЬреЗ admissions edit рдХрд░рдгреЗ. Events "рдорд▓рд╛ рд╕рдЧрд│реНрдпрд╛рдВрдирд╛ рд╕рд╛рдВрдЧрд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗ" рдЪреЗ рд░реВрдкрд╛рдВрддрд░ "рдореА рдПрдХрджрд╛рдЪ рдЬрд╛рд╣реАрд░ рдХрд░рддреЗ" рдордзреНрдпреЗ рдХрд░рддрд╛рдд. Reads рдЖрдгрд┐ writes рдирд╛ рдЦреВрдк рд╡реЗрдЧрд│реЗ рдЖрдХрд╛рд░ рд╣рд╡реЗ рдЕрд╕рддрд╛рдд рддреЗрд╡реНрд╣рд╛ CQRS рдорджрдд рдХрд░рддреЗ тАФ report card screen рд▓рд╛ рд╕рд░рд╛рд╕рд░реА рд╣рд╡реА, рдирд┐рдпрдорд╛рдВрдирд╛ рд╕реНрд╡рддрдВрддреНрд░ рдЧреБрдг рд╣рд╡реЗрдд. рджреЛрдШрд╛рдВрдЪреАрд╣реА рдЦрд░реА рдХрд┐рдВрдордд рдЖрд╣реЗ: рдкреНрд░рд╡рд╛рд╣ рд╕рдордЬрд╛рдпрд▓рд╛ рдХрдареАрдг рд╣реЛрддреЛ, рдЖрдгрд┐ рд╡рд╛рдЪрдирд╛рдЪреА рдмрд╛рдЬреВ stale рдЕрд╕реВ рд╢рдХрддреЗ.

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

arch/models.py рдордзрд▓рд╛ EventBus рдкреНрд░рддреНрдпреЗрдХ event рдирд╛рд╡рд╛рд╕рд╛рдареА subscribers рдЪреА рдпрд╛рджреА рдареЗрд╡рддреЛ рдЖрдгрд┐ рддреНрдпрд╛рдВрдирд╛ рддреНрдпрд╛рдВрдиреА subscribe рдХреЗрд▓реЗрд▓реНрдпрд╛ рдХреНрд░рдорд╛рдиреЗрдЪ call рдХрд░рддреЛ (рдореНрд╣рдгреВрди output рдирд┐рд╢реНрдЪрд┐рдд рдЕрд╕рддреЛ; рдЦрд░реЗ brokers asynchronously рдкреЛрд╣реЛрдЪрд╡рддрд╛рдд). ReportCardProjection MarkRecorded events pending рдордзреНрдпреЗ рд░рд╛рдВрдЧреЗрдд рдареЗрд╡рддреЗ рдЖрдгрд┐ рддреБрдореНрд╣реА catch_up(n) call рдХрд░рддрд╛ рддреЗрд╡реНрд╣рд╛рдЪ рддреЗ рд▓рд╛рдЧреВ рдХрд░рддреЗ тАФ рдПрдХрд╛ рд╡реЗрд│реА n events тАФ рддреНрдпрд╛рдореБрд│реЗ read model рдмрд░реЛрдмрд░реАрд▓рд╛ рдпреЗрдгреНрдпрд╛рдЪреНрдпрд╛ рдЖрдзреА рдХрд╕реЗ рджрд┐рд╕рддреЗ рддреЗ рддреБрдореНрд╣реА рдкрд╛рд╣реВ рд╢рдХрддрд╛.

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

python3 arch/demo.py events
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from models import EventBus, ReportCardProjection
bus = EventBus()
for who in ("grades", "fees", "timetable", "library"):
    bus.subscribe("StudentAdmitted", who, lambda d, w=who: f"{w} saw {d['student']}")
print(bus.publish("StudentAdmitted", {"student": "Aishwarya"}))
p = ReportCardProjection()
for v in (72, 85, 90): p.on_mark({"student": "Katrina", "value": v})
print("applied 0 тЖТ", p.average("Katrina"))
for _ in range(3):
    p.catch_up(1); print(f"applied {p.applied} тЖТ {p.average('Katrina')}")
EOF

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

events рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА admissions publishes StudentAdmitted(Dipika, 5A) on the notice board тЖТ grades opens a mark sheet ┬╖ fees opens an account ┬╖ timetable assigns a class
   direct calls: admissions imports 3 departments ┬╖ events: it imports 0 of them (only the event's shape) тАФ a 4th listener needs no change to admissions
тФАтФА CQRS: 3 marks written (72, 85, 90); the read model has applied 2 тЖТ average 78.5
   after catch-up it has applied 3 тЖТ average 82.33 ┬╖ reads are fast, and eventually consistent

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

[('grades', 'grades saw Aishwarya'), ('fees', 'fees saw Aishwarya'), ('timetable', 'timetable saw Aishwarya'), ('library', 'library saw Aishwarya')]
applied 0 тЖТ None
applied 1 тЖТ 72.0
applied 2 тЖТ 78.5
applied 3 тЖТ 82.33

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

рд╡рд╛рдЪрдирд╛рд▓рдп рдЪреМрдерд╛ listener рдореНрд╣рдгреВрди рдЬреЛрдбрд▓реЗ рдЧреЗрд▓реЗ рдЖрдгрд┐ admissions рдмрджрд▓рд▓реЗ рдирд╛рд╣реА тАФ publisher рдЪреНрдпрд╛ рд╡рд┐рднрд╛рдЧрд╛рдВрдХрдбреЗ рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдмрд╛рд╣реЗрд░рдЪреНрдпрд╛ рдорд╛рд░реНрдЧрд┐рдХрд╛ 3 рд╡рд░реВрди 0 рдЭрд╛рд▓реНрдпрд╛. Coupling рдирд╛рд╣реАрд╕реЗ рдЭрд╛рд▓реЗ рдирд╛рд╣реА: рдкреНрд░рддреНрдпреЗрдХ listener рдЖрддрд╛ StudentAdmitted рдЪреНрдпрд╛ рдЖрдХрд╛рд░рд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗ, рдореНрд╣рдгреВрди рддреНрдпрд╛ рдЖрдХрд╛рд░рд╛рдЪреА API рдЗрддрдХреАрдЪ рдХрд╛рд│рдЬреА рдШреНрдпрд╛рдпрд▓рд╛ рд╣рд╡реА. рдЖрдгрд┐ рдмрд░реЛрдмрд░реАрд▓рд╛ рдпреЗрдд рдЕрд╕рддрд╛рдирд╛ read model рдиреЗ рдПрдХрд╛рдЪ рдкреНрд░рд╢реНрдирд╛рдЪреА рддреАрди рд╡реЗрдЧрд╡реЗрдЧрд│реА рдЙрддреНрддрд░реЗ рджрд╛рдЦрд╡рд▓реА: рдХрд╛рд╣реАрдЪ рдирд╛рд╣реА (None), 72.0, 78.5, рдЖрдгрд┐ рд╢реЗрд╡рдЯреА 82.33. рд╢рд┐рдХреНрд╖рд┐рдХреЗрдиреЗ рдЧреБрдг save рдХреЗрд▓реНрдпрд╛рд╡рд░ рд▓рдЧреЗрдЪ рддреНрдпрд╛рддреВрди рд╡рд╛рдЪрдгрд╛рд░реА screen рдЬреБрдиреА рд╕рд░рд╛рд╕рд░реА рджрд╛рдЦрд╡реВ рд╢рдХрддреЗ тАФ рддреБрдореНрд╣реА рддреНрдпрд╛рд╕рд╛рдареА design рдХрд░рддрд╛ ("updatingтАж" рджрд╛рдЦрд╡рд╛, рдХрд┐рдВрд╡рд╛ рд╕реНрд╡рддрдГрдЪреЗ write, write рдмрд╛рдЬреВрдХрдбреВрдирдЪ рд╡рд╛рдЪрд╛).

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

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

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ рд╕рд╛рдзреЗ read model PostgreSQL рдордзрд▓реЗ materialized view рдЕрд╕реВ рд╢рдХрддреЗ, рд╡реЗрд│рд╛рдкрддреНрд░рдХрд╛рдиреБрд╕рд╛рд░ рдХрд┐рдВрд╡рд╛ writes рдирдВрддрд░ refresh рдХреЗрд▓реЗрд▓реЗ (CONCURRENTLY рдкреНрд░рдХрд╛рд░рд╛рд▓рд╛ unique index рд▓рд╛рдЧрддреЛ, рдЖрдгрд┐ рддреЛ readers рдирд╛ block рдХрд░рдд рдирд╛рд╣реА):

CREATE MATERIALIZED VIEW report_cards AS
  SELECT roll_no, count(*) AS marks, round(avg(value), 2) AS average
  FROM marks GROUP BY roll_no;
CREATE UNIQUE INDEX ON report_cards (roll_no);
REFRESH MATERIALIZED VIEW CONCURRENTLY report_cards;

рд╡рд┐рднрд╛рдЧрд╛рдВрдордзрд▓реЗ events рд╕рд╣рд╕рд╛ broker рдордзреВрди рдЬрд╛рддрд╛рдд. Kafka рд╕реЛрдмрдд (рдкреНрд░рддреНрдпреЗрдХ event рдкреНрд░рдХрд╛рд░рд╛рд╕рд╛рдареА рдПрдХ topic, рдРрдХрдгрд╛рд▒реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рд╕рд╛рдареА рдПрдХ consumer group):

kafka-topics.sh --bootstrap-server localhost:9092 --create --topic student-admitted --partitions 3
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic student-admitted --group library

Event рдЪреЗ рд╡рд░реНрдгрди рдХрд░рд╛рд░ рдореНрд╣рдгреВрди рдХрд░рд╛ тАФ рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде schema registry рдордзреАрд▓ JSON Schema рдХрд┐рдВрд╡рд╛ Avro schema рд╕реЛрдмрдд тАФ рдЖрдгрд┐ рддреНрдпрд╛рд▓рд╛ version рджреНрдпрд╛ (StudentAdmitted v2 рдПрдХ field рдЬреЛрдбрддреЗ; рдЬреБрдиреЗ listeners рддреНрдпрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░рддрд╛рдд).

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ event рд╕рд╛рдареА рд▓рд┐рд╣реВрди рдареЗрд╡рд╛ тАФ рддреЛ рдХреЛрдг publish рдХрд░рддреЗ, рдХреЛрдг рдРрдХрддреЗ, рддреНрдпрд╛рдЪреЗ schema version, рдЖрдгрд┐ рд╡рд╛рдЪрдирд╛рдЪреА рдмрд╛рдЬреВ рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд рдХрд┐рддреА lag рджрд╛рдЦрд╡реВ рд╢рдХрддреЗ. "рдХреЛрдг рдРрдХрддреЗ?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рдХреЛрдгрд╛рд▓рд╛рдЪ рджреЗрддрд╛ рдпреЗрдд рдирд╕реЗрд▓, рддрд░ рддреБрдореНрд╣реА рддреБрдордЪреНрдпрд╛ рдорд╛рд░реНрдЧрд┐рдХрд╛рдВрдЪрд╛ рдорд╛рдЧ рдЧрдорд╛рд╡рд▓рд╛ рдЖрд╣реЗ.

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

Events рд╡рд┐рднрд╛рдЧрд╛рдВрдордзреНрдпреЗ data рд╡рд╛рд╣реВрди рдиреЗрддрд╛рдд. рдордЧ data рдЪреА рдорд╛рд▓рдХреА рдХреЛрдгрд╛рдХрдбреЗ тАФ рдЖрдгрд┐ рджреБрд╕рд░рд╛ рд╡рд┐рднрд╛рдЧ рддреНрдпрд╛рдЪреЗ рдХрдкрд╛рдЯ рдереЗрдЯ рд╡рд╛рдЪреВ рд╢рдХрддреЛ рдХрд╛? Data ownership рдЖрдгрд┐ integration.

git checkout lesson-08-data-ownership

ЁЯУМ Lesson 07 тАФ Event-driven architecture and CQRS: the notice board and the display board

ЁЯУН You are here: Lesson 07 of 12 ┬╖ Previous: lesson-06-styles ┬╖ Next: lesson-08-data-ownership


ЁЯУж What's in this branch

Lessons 01тАУ06, plus a different kind of corridor. Instead of admissions calling grades, fees and timetable, it publishes an event тАФ "a student was admitted" тАФ and whoever cares subscribes. Then CQRS: keep the model you write to separate from the model you read from, and accept that the read side is a little behind. EventBus and ReportCardProjection in arch/models.py, events() in arch/demo.py. The System Design school's lesson on events, pub/sub and the outbox covers delivery and the outbox; this lesson is about what events do to coupling.

ЁЯзТ Explain like I'm 5

When a new child joins the school, the admissions office used to walk to the exam cell, then to the accounts office, then to the timetable office, and tell each one. ЁЯЪ╢тАНтЩАя╕П If a new library opened, admissions had to learn a fourth walk.

Now there is a notice board ЁЯУМ in the hall. Admissions pins one notice: "Dipika joined class 5A." That's it. The exam cell, the accounts office and the timetable office each read the board and do their own job. When the library opens, the librarian just starts reading the board too тАФ admissions does not change at all.

And the marks? The teacher writes marks in the register (writing). The display board in the hall shows each child's average (reading). Someone updates the display board a little later. For a few minutes, the board can show an old average. That is the price of a fast, simple board.

ЁЯЧ║я╕П Diagram

flowchart LR
    adm["ЁЯЧВя╕П admissions"] -->|"publish StudentAdmitted(Dipika, 5A)"| board["ЁЯУМ notice board<br/>(event bus)"]
    board --> gr["ЁЯУЭ grades: opens a mark sheet"]
    board --> fee["ЁЯТ░ fees: opens an account"]
    board --> tt["ЁЯЧУя╕П timetable: assigns a class"]
    board -.-> lib["ЁЯУЦ library (new): no change to admissions"]
    subgraph cqrs["CQRS"]
      w["тЬНя╕П write side: marks 72, 85, 90"] -->|"MarkRecorded events"| proj["ЁЯФД projection"]
      proj --> r["ЁЯУ║ read side: average 78.5 тЖТ 82.33"]
    end

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

тЭУ What

ЁЯдФ Why

Because every direct call is a corridor from the caller to the callee тАФ admissions with three calls has three outgoing corridors, and a fourth department means editing admissions. Events turn "I must tell everyone" into "I announce once". CQRS helps when reads and writes want very different shapes тАФ a report card screen wants averages, the rules want individual marks. Both have a real price: the flow is harder to follow, and the read side can be stale.

ЁЯФз How (in this repo)

EventBus in arch/models.py keeps a list of subscribers per event name and calls them in the order they subscribed (so the output is deterministic; real brokers deliver asynchronously). ReportCardProjection queues MarkRecorded events in pending and applies them only when you call catch_up(n) тАФ n events at a time тАФ so you can see the read model before it has caught up.

ЁЯзк Try it

python3 arch/demo.py events
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from models import EventBus, ReportCardProjection
bus = EventBus()
for who in ("grades", "fees", "timetable", "library"):
    bus.subscribe("StudentAdmitted", who, lambda d, w=who: f"{w} saw {d['student']}")
print(bus.publish("StudentAdmitted", {"student": "Aishwarya"}))
p = ReportCardProjection()
for v in (72, 85, 90): p.on_mark({"student": "Katrina", "value": v})
print("applied 0 тЖТ", p.average("Katrina"))
for _ in range(3):
    p.catch_up(1); print(f"applied {p.applied} тЖТ {p.average('Katrina')}")
EOF

тЬЕ Verify тАФ what you should see

events prints:

тФАтФА admissions publishes StudentAdmitted(Dipika, 5A) on the notice board тЖТ grades opens a mark sheet ┬╖ fees opens an account ┬╖ timetable assigns a class
   direct calls: admissions imports 3 departments ┬╖ events: it imports 0 of them (only the event's shape) тАФ a 4th listener needs no change to admissions
тФАтФА CQRS: 3 marks written (72, 85, 90); the read model has applied 2 тЖТ average 78.5
   after catch-up it has applied 3 тЖТ average 82.33 ┬╖ reads are fast, and eventually consistent

Your snippet prints:

[('grades', 'grades saw Aishwarya'), ('fees', 'fees saw Aishwarya'), ('timetable', 'timetable saw Aishwarya'), ('library', 'library saw Aishwarya')]
applied 0 тЖТ None
applied 1 тЖТ 72.0
applied 2 тЖТ 78.5
applied 3 тЖТ 82.33

ЁЯПБ What you just proved

The library joined as a fourth listener and admissions did not change тАФ the publisher's outgoing corridors to departments went from 3 to 0. The coupling did not vanish: every listener now depends on the shape of StudentAdmitted, so that shape needs the same care as an API. And the read model showed three different answers for the same question while it caught up: nothing (None), 72.0, 78.5, and finally 82.33. A screen that reads from it right after a teacher saves a mark can show an old average тАФ you design for that (show "updatingтАж", or read your own write from the write side).

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ a simple read model can be a materialized view in PostgreSQL, refreshed on a schedule or after writes (the CONCURRENTLY form needs a unique index, and does not block readers):

CREATE MATERIALIZED VIEW report_cards AS
  SELECT roll_no, count(*) AS marks, round(avg(value), 2) AS average
  FROM marks GROUP BY roll_no;
CREATE UNIQUE INDEX ON report_cards (roll_no);
REFRESH MATERIALIZED VIEW CONCURRENTLY report_cards;

Events between departments usually go through a broker. With Kafka (a topic per event type, one consumer group per listening department):

kafka-topics.sh --bootstrap-server localhost:9092 --create --topic student-admitted --partitions 3
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic student-admitted --group library

Describe the event as a contract тАФ for example with a JSON Schema or an Avro schema in a schema registry тАФ and version it (StudentAdmitted v2 adds a field; old listeners ignore it).

ЁЯПн Why this matters in production: for each event, write down who publishes it, who listens, its schema version, and the worst lag the read side may show. If nobody can answer "who listens?", you have lost track of your corridors.

тПня╕П Next

Events carry data between departments. So who owns the data тАФ and may another department read its cupboard directly? Data ownership and integration.

git checkout lesson-08-data-ownership
тЖР PreviousstylesNext тЖТdata ownership

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