ЁЯПл The SchoolтА║ЁЯПЫя╕П Software ArchitectureтА║ЁЯЧВя╕П рдзрдбрд╛ 05 тАФ Domain-driven design: рд╕реНрд╡рддрдГрдЪреЗ рд╢рдмреНрдж рдЕрд╕рд▓реЗрд▓реЗ рд╡рд┐рднрд╛рдЧ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЧВя╕П рдзрдбрд╛ 05 тАФ Domain-driven design: рд╕реНрд╡рддрдГрдЪреЗ рд╢рдмреНрдж рдЕрд╕рд▓реЗрд▓реЗ рд╡рд┐рднрд╛рдЧ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-hexagonal ┬╖ рдкреБрдвреЗ: lesson-06-styles


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

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ рд╡рд┐рднрд╛рдЧ. рд╢рд╛рд│рд╛ рдореНрд╣рдгрдЬреЗ рдПрдХ рдореЛрдареА рдирд┐рдпрдордкреБрд╕реНрддрд┐рдХрд╛ рдирд╡реНрд╣реЗ тАФ рддреА рдЖрд╣реЗ рдПрдХ рдкреНрд░рд╡реЗрд╢ рдХрд╛рд░реНрдпрд╛рд▓рдп, рдПрдХ рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧ, рдПрдХ рд▓реЗрдЦрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрдгрд┐ рдПрдХ рд╡реЗрд│рд╛рдкрддреНрд░рдХ рдХрд╛рд░реНрдпрд╛рд▓рдп, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдЬрдг рддреЗрдЪ рд╢рдмреНрдж рд╡реЗрдЧрд│реНрдпрд╛ рдЕрд░реНрдерд╛рдиреЗ рд╡рд╛рдкрд░рддреЛ. Domain-driven design (DDD) рдкреНрд░рддреНрдпреЗрдХрд╛рднреЛрд╡рддреА рдПрдХ bounded context рдЖрдЦрддреЗ, рддреНрдпрд╛рдЪреНрдпрд╛ рдЖрдд рдПрдХ ubiquitous language рдмреЛрд▓рддреЗ, рдЖрдгрд┐ рддреЗ рдПрдХрдореЗрдХрд╛рдВрд╢реА рдХрд╕реЗ рдЬреЛрдбрд▓реЗрд▓реЗ рдЖрд╣реЗрдд рдпрд╛рдЪрд╛ context map рдХрд╛рдврддреЗ. Lab рд╣рд╛ context map code рдордзреВрди рд╡рд╛рдЪрддреЛ: arch/analyze.py рдордзреАрд▓ CONTEXTS рдЖрдгрд┐ context_edges(), arch/demo.py рдордзреАрд▓ ddd().

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

рддреАрди offices рдирд╛ рд╡рд┐рдЪрд╛рд░рд╛ "student рдореНрд╣рдгрдЬреЗ рдХрд╛рдп?" ЁЯдФ

рддрд┐рдШреЗрд╣реА рдмрд░реЛрдмрд░ рдЖрд╣реЗрдд тАФ рдЖрдкрд╛рдкрд▓реНрдпрд╛ office рдЪреНрдпрд╛ рдЖрдд. рдЧрдбрдмрдб рддреЗрд╡реНрд╣рд╛ рд╕реБрд░реВ рд╣реЛрддреЗ рдЬреЗрд╡реНрд╣рд╛ рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧ рдкреНрд░рд╡реЗрд╢ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреА student рдЪреА рдХрд▓реНрдкрдирд╛ рдЙрд╕рдиреА рдШреЗрддреЛ. рдЖрддрд╛ рдкреНрд░рд╡реЗрд╢ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдиреЗ рдЖрдкрд▓реНрдпрд╛ form рдордзреНрдпреЗ рдПрдХ рдирд╡рд╛ рд░рдХрд╛рдирд╛ рдЬреЛрдбрд▓рд╛ рдХреА рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧрд╛рдЪреЗ report cards рдмрд┐рдШрдбреВ рд╢рдХрддрд╛рдд, рдЖрдгрд┐ рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧрд╛рдд рдХреЛрдгреАрд╣реА рдХрд╛рд╣реАрдЪ рдмрджрд▓рд▓реЗрд▓реЗ рдирд╕рддреЗ.

рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ office student рдЪреА рд╕реНрд╡рддрдГрдЪреА рдХрд▓реНрдкрдирд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рд╢рдмреНрджрд╛рдВрдд рдареЗрд╡рддреЗ. рдкрд░реАрдХреНрд╖рд╛ рд╡рд┐рднрд╛рдЧрд╛рд▓рд╛ рдкреНрд░рд╡реЗрд╢ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдХрдбреВрди рдирд╛рд╡ рд╣рд╡реЗ рдЕрд╕реЗрд▓, рддреЗрд╡реНрд╣рд╛ рджрд╛рд░рд╛рд╡рд░ рдХреЛрдгреАрддрд░реА рдЕрдиреБрд╡рд╛рдж рдХрд░рддреЗ: "рддреБрдордЪрд╛ рдЕрд░реНрдЬрджрд╛рд░ рдХреНрд░рдорд╛рдВрдХ 1042 рдореНрд╣рдгрдЬреЗ рдЖрдордЪрд╛ roll рдХреНрд░рдорд╛рдВрдХ 7." рддреЛ рдЕрдиреБрд╡рд╛рджрдХ рдореНрд╣рдгрдЬреЗ anti-corruption layer.

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

flowchart LR
    adm["ЁЯЧВя╕П admissions<br/>Student = form no, name, status"]
    gr["ЁЯУЭ grades (exam cell)<br/>student = roll no + marks"]
    fee["ЁЯТ░ fees (accounts)<br/>student = an account"]
    tt["ЁЯЧУя╕П timetable<br/>periods and rooms"]
    gr -->|"imports Student ┬╖ conformist today<br/>тЖТ anti-corruption layer"| adm
    fee -->|"imports Student ┬╖ customerтАУsupplier"| adm

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд╕рдВрдкреВрд░реНрдг рд╢рд╛рд│реЗрд╕рд╛рдареА рдПрдХрдЪ model рдореНрд╣рдгрдЬреЗ рдЪрд╛рд│реАрд╕ fields рдЕрд╕рд▓реЗрд▓рд╛ Student class, рдЬреЛ рдкреНрд░рддреНрдпреЗрдХ office edit рдХрд░рддреЗ рдЖрдгрд┐ рдХреЛрдгрд╛рдЪреАрдЪ рдорд╛рд▓рдХреА рдирд╕рддреЗ. Bounded contexts рдореБрд│реЗ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧ рд╕рдВрдкреВрд░реНрдг рд╢рд╛рд│реЗрдЪреНрдпрд╛ meeting рд╢рд┐рд╡рд╛рдп рдЖрдкрд▓реЗ рдирд┐рдпрдо рдмрджрд▓реВ рд╢рдХрддреЛ тАФ рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рд╕рд╛рдареА modifiability. рдирдВрддрд░ code рддреЛрдбрдгреНрдпрд╛рд╕рд╛рдареАрдЪреЗ (рдзрдбрд╛ 06) рддреЗ рдиреИрд╕рд░реНрдЧрд┐рдХ рд╕рд╛рдВрдзреЗрд╣реА рдЖрд╣реЗрдд: рд╡рд┐рднрд╛рдЧрд╛рдВрдиреБрд╕рд╛рд░ рддреЛрдбрд╛, рдордЬрд▓реНрдпрд╛рдВрдиреБрд╕рд╛рд░ рдирд╡реНрд╣реЗ.

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

arch/analyze.py рдордзреАрд▓ CONTEXTS рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рд▓рд╛ рддреНрдпрд╛рдЪреНрдпрд╛ рдЦреЛрд▓реНрдпрд╛рдВрд╢реА рдЬреЛрдбрддреЛ (grades рдХрдбреЗ domain.grades, app.report_card рдЖрдгрд┐ infra.db рдЪреА рдорд╛рд▓рдХреА). context_edges(g, names) рдПрдХрд╛ рд╡рд┐рднрд╛рдЧрд╛рддреВрди рджреБрд╕рд▒реНрдпрд╛ рд╡рд┐рднрд╛рдЧрд╛рдд рдЬрд╛рдгрд╛рд░реА рдкреНрд░рддреНрдпреЗрдХ рдорд╛рд░реНрдЧрд┐рдХрд╛, import рдХреЗрд▓реЗрд▓реНрдпрд╛ рдирд╛рд╡рд╛рдВрд╕рд╣, рд╕реВрдЪреАрдмрджреНрдз рдХрд░рддреЛ тАФ scan() рддреА рдкреНрд░рддреНрдпреЗрдХ from тАж import тАж рдордзреВрди рдиреЛрдВрджрд╡рддреЛ. Demo рдордзреАрд▓ "student" рдЪреЗ рдЕрд░реНрде рд╣рд╛рддрд╛рдиреЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ рдЖрд╣реЗрдд (рднрд╛рд╖рд╛ рдорд╛рдгреВрд╕ рдард░рд╡рддреЛ, scanner рдирд╡реНрд╣реЗ); edges рдорд╛рддреНрд░ code рдордзреВрди рд╡рд╛рдЪрд▓реЗ рдЬрд╛рддрд╛рдд.

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

python3 arch/demo.py ddd
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, CONTEXTS, context_edges, apply
cb = scan(); g = cb.imports
for c, rooms in CONTEXTS.items():
    print(f"{c:<10} rooms {rooms} ┬╖ classes {[k for r in rooms for k, _ in cb.classes[r]]}")
print("cross-department corridors:", len(context_edges(g, cb.names)))
acl = apply(g, remove=[("domain.grades", "domain.admissions"), ("domain.fees", "domain.admissions")])
print("after each department keeps its own Student:", len(context_edges(acl)))
EOF

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

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

тФАтФА the context map, read from the code: 2 cross-department corridors
   fees тЖТ admissions: domain.fees imports Student from domain.admissions
   grades тЖТ admissions: domain.grades imports Student from domain.admissions

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

admissions rooms ['domain.admissions', 'app.enrol'] ┬╖ classes ['Student', 'Application', 'EnrolService']
grades     rooms ['domain.grades', 'app.report_card', 'infra.db'] ┬╖ classes ['Mark', 'ReportCard', 'ReportCardService', 'SqlGradeStore', 'SqlLedger']
fees       rooms ['domain.fees', 'app.fees_service'] ┬╖ classes ['Account', 'FeesService']
timetable  rooms ['domain.timetable'] ┬╖ classes ['Period']
cross-department corridors: 2
after each department keeps its own Student: 0

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

Context map рдореНрд╣рдгрдЬреЗ whiteboard рд╡рд░рдЪреА рдЗрдЪреНрдЫрд╛ рдирд╡реНрд╣реЗ тАФ рддреЛ imports рдордзреВрди рд╡рд╛рдЪрддрд╛ рдпреЗрддреЛ. рджреЛрди рд╡рд┐рднрд╛рдЧ рдПрдХрд╛рдЪ class рд╕рд╛рдареА, Student, admissions рдордзреНрдпреЗ рд╣рд╛рдд рдШрд╛рд▓рддрд╛рдд. рджреЛрдШрд╛рдВрдирд╛рд╣реА рдлрдХреНрдд рдирд╛рд╡ рд╣рд╡реЗ рдЖрд╣реЗ, рдкрдг рдЖрддрд╛ admissions рдиреЗ рдЖрдкрд▓рд╛ model рдмрджрд▓рд▓рд╛ рдХреА рджреЛрдШреЗрд╣реА рдмрджрд▓рддрд╛рдд. рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рд▓рд╛ student рдЪреА рд╕реНрд╡рддрдГрдЪреА рдЫреЛрдЯреА рдХрд▓реНрдкрдирд╛ рджреНрдпрд╛ (рдЖрдгрд┐ рджрд╛рд░рд╛рд╡рд░ рдЕрдиреБрд╡рд╛рдж рдХрд░рд╛) рдЖрдгрд┐ рд╡рд┐рднрд╛рдЧ-рдУрд▓рд╛рдВрдбрдгрд╛рд▒реНрдпрд╛ рдорд╛рд░реНрдЧрд┐рдХрд╛ 2 рд╡рд░реВрди 0 рд╡рд░ рдпреЗрддрд╛рдд. рдпрд╛рджреАрдд рджрд┐рд╕рдгрд╛рд░реА рдПрдХ рд╡рд┐рдЪрд┐рддреНрд░ рдЧреЛрд╖реНрдЯ рд▓рдХреНрд╖рд╛рдд рдШреНрдпрд╛: SqlLedger grades рд╡рд┐рднрд╛рдЧрд╛рдЪреНрдпрд╛ infra.db рдордзреНрдпреЗ рд░рд╛рд╣рддреЛ, рдкрдг рддреЛ fees рдЪреА рд╕реЗрд╡рд╛ рдХрд░рддреЛ. File technology рдиреБрд╕рд╛рд░ рдорд╛рдВрдбрд▓реЗрд▓реА рдЖрд╣реЗ, рд╡рд┐рднрд╛рдЧрд╛рдиреБрд╕рд╛рд░ рдирд╡реНрд╣реЗ. рддреБрдореНрд╣реА рд╡рд┐рднрд╛рдЧрд╛рдВрдиреБрд╕рд╛рд░ рддреЛрдбрддрд╛, рддреЗрд╡реНрд╣рд╛ рддреА file рд╣реА рддреБрдЯрддреЗ.

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

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

On a real project тАФ anti-corruption layer рдореНрд╣рдгрдЬреЗ рдмрд╣реБрддреЗрдХрджрд╛ downstream рд╡рд┐рднрд╛рдЧрд╛рдЪреНрдпрд╛ рдорд╛рд▓рдХреАрдЪреЗ рдлрдХреНрдд рдПрдХ рдЫреЛрдЯреЗ рдЕрдиреБрд╡рд╛рджрдХ module:

# grades/acl/admissions.py тАФ the only file in grades that knows admissions' model
from dataclasses import dataclass
from admissions.api import get_student          # the upstream's published interface

@dataclass(frozen=True)
class Learner:                                  # grades' own word
    roll_no: int
    display_name: str

def learner_for(roll_no: int) -> Learner:
    s = get_student(roll_no)                    # upstream shape: form_no, full_name, statusтАж
    return Learner(roll_no=roll_no, display_name=s["full_name"])

рд╡рд┐рднрд╛рдЧрд╛рдВрдирд╛ top-level packages рдмрдирд╡рд╛ (schoolapp/admissions/, schoolapp/grades/ тАж) рдЖрдгрд┐ import-linter рд▓рд╛ рддреНрдпрд╛рдВрдирд╛ рд╡реЗрдЧрд│реЗ рдареЗрд╡реВ рджреНрдпрд╛, рдлрдХреНрдд рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рдЪреНрдпрд╛ api module рд▓рд╛ рдкрд░рд╡рд╛рдирдЧреА рджреЗрдКрди:

[importlinter:contract:departments]
name = Departments talk only through their api
type = independence
modules =
    schoolapp.admissions
    schoolapp.grades
    schoolapp.fees
    schoolapp.timetable
ignore_imports =
    schoolapp.grades.acl.admissions -> schoolapp.admissions.api

Contexts рд╢реЛрдзрдгреЗ рд╣реА workshop рдЖрд╣реЗ, script рдирд╡реНрд╣реЗ: Event Storming (Alberto Brandolini) domain рддрдЬреНрдЬреНрдЮ рдЖрдгрд┐ developers рдирд╛ sticky notes рдЪреНрдпрд╛ рдПрдХрд╛ рд▓рд╛рдВрдм рднрд┐рдВрддреАрд╕рдореЛрд░ рдЙрднреЗ рдХрд░рддреЗ, рдкреНрд░рддреНрдпреЗрдХ business event рд╕рд╛рдареА рдПрдХ note, рдЖрдгрд┐ рдЬрд┐рдереЗ рд╢рдмреНрдж рдмрджрд▓рддрд╛рдд рддрд┐рдереЗ рд╕реАрдорд╛ рджрд┐рд╕реВ рд▓рд╛рдЧрддрд╛рдд.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рддреБрдордЪреЗ рд╕рдЧрд│реНрдпрд╛рдд рдЬрд╛рд╕реНрдд import рд╣реЛрдгрд╛рд░реЗ рдкрд╛рдЪ classes рд▓рд┐рд╣рд╛. рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рддреАрди рд╡рд┐рднрд╛рдЧрд╛рдВрдирд╛ рд╡рд┐рдЪрд╛рд░рд╛ рдХреА рддреНрдпрд╛ рд╢рдмреНрджрд╛рдЪрд╛ рдЕрд░реНрде рдХрд╛рдп. рджреЛрди рд╡реЗрдЧрд│реА рдЙрддреНрддрд░реЗ рдореНрд╣рдгрдЬреЗ рджреЛрди models.

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

рд╡рд┐рднрд╛рдЧ рдореНрд╣рдгрдЬреЗ рд╕рд╛рдВрдзреЗ. рддреЗ рдПрдХрд╛рдЪ рдЗрдорд╛рд░рддреАрдд рд░рд╛рд╣рддрд╛рдд, рдХреА campus рднрд░ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдЗрдорд╛рд░рддреАрдВрдд? Monolith, modular monolith, microservices тАФ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреА рдХрд┐рдВрдордд.

git checkout lesson-06-styles

ЁЯЧВя╕П Lesson 05 тАФ Domain-driven design: departments with their own words

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-hexagonal ┬╖ Next: lesson-06-styles


ЁЯУж What's in this branch

Lessons 01тАУ04, plus departments. The school is not one big rule book тАФ it is an admissions office, an exam cell, an accounts office and a timetable office, and each one uses the same words differently. Domain-driven design (DDD) draws a bounded context around each, speaks a ubiquitous language inside it, and draws a context map of how they relate. The lab reads the context map from the code: CONTEXTS and context_edges() in arch/analyze.py, ddd() in arch/demo.py.

ЁЯзТ Explain like I'm 5

Ask three offices "what is a student?" ЁЯдФ

All three are right тАФ inside their own office. Trouble starts when the exam cell borrows the admissions office's idea of a student. Now, when admissions adds a new box to its form, the exam cell's report cards can break, and nobody in the exam cell changed anything.

So each office keeps its own idea of a student, in its own words. When the exam cell needs a name from admissions, someone at the door translates: "your applicant number 1042 is our roll number 7." That translator is an anti-corruption layer.

ЁЯЧ║я╕П Diagram

flowchart LR
    adm["ЁЯЧВя╕П admissions<br/>Student = form no, name, status"]
    gr["ЁЯУЭ grades (exam cell)<br/>student = roll no + marks"]
    fee["ЁЯТ░ fees (accounts)<br/>student = an account"]
    tt["ЁЯЧУя╕П timetable<br/>periods and rooms"]
    gr -->|"imports Student ┬╖ conformist today<br/>тЖТ anti-corruption layer"| adm
    fee -->|"imports Student ┬╖ customerтАУsupplier"| adm

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

тЭУ What

ЁЯдФ Why

Because one model for the whole school becomes a Student class with forty fields that every office edits and nobody owns. Bounded contexts let each department change its rules without a school-wide meeting тАФ which is modifiability, per department. They are also the natural seams for splitting code later (lesson 06): split along departments, not along floors.

ЁЯФз How (in this repo)

CONTEXTS in arch/analyze.py maps each department to its rooms (grades owns domain.grades, app.report_card and infra.db). context_edges(g, names) lists every corridor that crosses from one department to another, with the names imported тАФ scan() records them from each from тАж import тАж. The meanings of "student" in the demo are written by hand (a person decides a language, not a scanner); the edges are read from the code.

ЁЯзк Try it

python3 arch/demo.py ddd
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, CONTEXTS, context_edges, apply
cb = scan(); g = cb.imports
for c, rooms in CONTEXTS.items():
    print(f"{c:<10} rooms {rooms} ┬╖ classes {[k for r in rooms for k, _ in cb.classes[r]]}")
print("cross-department corridors:", len(context_edges(g, cb.names)))
acl = apply(g, remove=[("domain.grades", "domain.admissions"), ("domain.fees", "domain.admissions")])
print("after each department keeps its own Student:", len(context_edges(acl)))
EOF

тЬЕ Verify тАФ what you should see

ddd prints:

тФАтФА the context map, read from the code: 2 cross-department corridors
   fees тЖТ admissions: domain.fees imports Student from domain.admissions
   grades тЖТ admissions: domain.grades imports Student from domain.admissions

Your snippet prints:

admissions rooms ['domain.admissions', 'app.enrol'] ┬╖ classes ['Student', 'Application', 'EnrolService']
grades     rooms ['domain.grades', 'app.report_card', 'infra.db'] ┬╖ classes ['Mark', 'ReportCard', 'ReportCardService', 'SqlGradeStore', 'SqlLedger']
fees       rooms ['domain.fees', 'app.fees_service'] ┬╖ classes ['Account', 'FeesService']
timetable  rooms ['domain.timetable'] ┬╖ classes ['Period']
cross-department corridors: 2
after each department keeps its own Student: 0

ЁЯПБ What you just proved

The context map is not a wish on a whiteboard тАФ it can be read from the imports. Two departments reach into admissions for the same class, Student. Both need only a name, but both now change whenever admissions changes its model. Give each department its own small idea of a student (and translate at the door) and the cross-department corridors drop from 2 to 0. Notice one oddity the listing shows: SqlLedger lives in the grades department's infra.db, but it serves fees. The file is organised by technology, not by department. When you split along departments, that file splits too.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ an anti-corruption layer is often just a small translator module owned by the downstream department:

# grades/acl/admissions.py тАФ the only file in grades that knows admissions' model
from dataclasses import dataclass
from admissions.api import get_student          # the upstream's published interface

@dataclass(frozen=True)
class Learner:                                  # grades' own word
    roll_no: int
    display_name: str

def learner_for(roll_no: int) -> Learner:
    s = get_student(roll_no)                    # upstream shape: form_no, full_name, statusтАж
    return Learner(roll_no=roll_no, display_name=s["full_name"])

Make departments top-level packages (schoolapp/admissions/, schoolapp/grades/ тАж) and let import-linter keep them apart, allowing only each department's api module:

[importlinter:contract:departments]
name = Departments talk only through their api
type = independence
modules =
    schoolapp.admissions
    schoolapp.grades
    schoolapp.fees
    schoolapp.timetable
ignore_imports =
    schoolapp.grades.acl.admissions -> schoolapp.admissions.api

Finding the contexts is a workshop, not a script: Event Storming (Alberto Brandolini) puts the domain experts and developers in front of a long wall of sticky notes, one per business event, and the boundaries show up where the words change.

ЁЯПн Why this matters in production: list your five most imported classes. For each, ask three departments what the word means. Two different answers means two models.

тПня╕П Next

Departments are the seams. Do they live in one building, or in separate buildings across campus? Monolith, modular monolith, microservices тАФ and what each one costs.

git checkout lesson-06-styles
тЖР PrevioushexagonalNext тЖТstyles

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