ЁЯПл The SchoolтА║ЁЯПЫя╕П Software ArchitectureтА║ЁЯПв рдзрдбрд╛ 03 тАФ Layers: рдордЬрд▓реЗ, рдЖрдгрд┐ рдЬрд┐рдиреЗ рдХреЛрдгрддреНрдпрд╛ рджрд┐рд╢реЗрдиреЗ рдЬрд╛рддрд╛рдд
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯПв рдзрдбрд╛ 03 тАФ Layers: рдордЬрд▓реЗ, рдЖрдгрд┐ рдЬрд┐рдиреЗ рдХреЛрдгрддреНрдпрд╛ рджрд┐рд╢реЗрдиреЗ рдЬрд╛рддрд╛рдд

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


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

рдзрдбреЗ 01тАУ02, рдЖрдгрд┐ рдПрдХ рдирд┐рдпрдо. рдкрд╛рд░рдВрдкрд░рд┐рдХ layered architecture code рдордЬрд▓реНрдпрд╛рдВрдордзреНрдпреЗ рд░рдЪрддреЗ тАФ рд╕рдЧрд│реНрдпрд╛рдд рд╡рд░ web (presentation), рдордЧ app (use cases), рдордЧ domain (business rules), рдЖрдгрд┐ рд╕рдЧрд│реНрдпрд╛рдд рдЦрд╛рд▓реА infra (database, email) тАФ рдЖрдгрд┐ рдПрдХрдЪ рдирд┐рдпрдо: рдПрдЦрд╛рджрд╛ рдордЬрд▓рд╛ рдлрдХреНрдд рддреНрдпрд╛рдЪреНрдпрд╛ рдЦрд╛рд▓рдЪреЗ рдордЬрд▓реЗ рд╡рд╛рдкрд░реВ рд╢рдХрддреЛ. Layer violations рдЖрдкреЛрдЖрдк рдХрд╕реЗ рд╢реЛрдзрд╛рдпрдЪреЗ, strict рд╡рд┐рд░реБрджреНрдз relaxed layering, рдЖрдгрд┐ рд╣рд╛ рдзрдбрд╛ рдПрдХрд╛ рдЖрд╢реНрдЪрд░реНрдпрд╛рдиреЗ рдХрд╛ рд╕рдВрдкрддреЛ. arch/analyze.py рдордзреАрд▓ layer_violations() рдЖрдгрд┐ arch/demo.py рдордзреАрд▓ layers().

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

рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдЗрдорд╛рд░рддреАрд▓рд╛ рдЪрд╛рд░ рдордЬрд▓реЗ рдЖрд╣реЗрдд. ЁЯПв

рдирд┐рдпрдо: рддреБрдореНрд╣реА рдЬрд┐рдиреНрдпрд╛рдиреЗ рдЦрд╛рд▓реА рдЬрд╛рдК рд╢рдХрддрд╛, рд╡рд░ рдХрдзреАрдЪ рдирд╛рд╣реА. Office рдирд┐рдпрдордкреБрд╕реНрддрд┐рдХрд╛ рдкрд╛рд╣реВ рд╢рдХрддреЗ. рдирд┐рдпрдордкреБрд╕реНрддрд┐рдХрд╛ рджрд┐рд╡рд╕рд╛рдЪреЗ рдирд╛рд╡ рдХрд╕реЗ рдЫрд╛рдкрд╛рдпрдЪреЗ рд╣реЗ рд╡рд┐рдЪрд╛рд░рд╛рдпрд▓рд╛ рд╕реНрд╡рд╛рдЧрдд рдХрдХреНрд╖рд╛рд▓рд╛ рдлреЛрди рдХрд░реВ рд╢рдХрдд рдирд╛рд╣реА.

рдРрд╢реНрд╡рд░реНрдпрд╛ clipboard рдШреЗрдКрди рдкреНрд░рддреНрдпреЗрдХ рдорд╛рд░реНрдЧрд┐рдХреЗрддреВрди рдлрд┐рд░рддреЗ рдЖрдгрд┐ рдЬрд┐рдиреНрдпрд╛рдиреЗ рд╡рд░ рдЬрд╛рдгрд╛рд░реА рдкреНрд░рддреНрдпреЗрдХ рдорд╛рд░реНрдЧрд┐рдХрд╛ рд▓рд┐рд╣реВрди рдареЗрд╡рддреЗ. рддреА рдпрд╛рджреА рдореНрд╣рдгрдЬреЗ layer violations.

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

flowchart TB
    web["ЁЯЦея╕П web тАФ reception (top floor)"]
    app["ЁЯзСтАНЁЯПл app тАФ the office"]
    domain["ЁЯУЪ domain тАФ the rule book"]
    infra["ЁЯФМ infra тАФ store room + post room (ground floor)"]
    web -->|"ok: down"| app
    app -->|"ok: down"| domain
    domain -->|"ok: down"| infra
    domain -.->|"тЭМ upward: timetable тЖТ web.format"| web
    infra -.->|"тЭМ upward ├Ч3: db тЖТ grades, db тЖТ ports, email тЖТ ports"| domain
    app -.->|"strict only: skips a floor (enrol тЖТ email)"| infra

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рджреЛрдиреНрд╣реА рджрд┐рд╢рд╛рдВрдиреА рдЬрд┐рдиреЗ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЗрдорд╛рд░рддреАрдЪреНрдпрд╛ рдирдХрд╛рд╢рд╛рд▓рд╛ рд╡рд░ рдирд╛рд╣реА рдЖрдгрд┐ рдЦрд╛рд▓реА рдирд╛рд╣реА: рднрд╛рдВрдбрд╛рд░ рддрдкрд╛рд╕рд▓реНрдпрд╛рд╢рд┐рд╡рд╛рдп рддреБрдореНрд╣реА рд╕реНрд╡рд╛рдЧрдд рдХрдХреНрд╖ рдмрджрд▓реВ рд╢рдХрдд рдирд╛рд╣реА, рдЖрдгрд┐ рддрд╛рд░рдЦрд╛ рдХрд╢рд╛ рдЫрд╛рдкрд╛рдпрдЪреНрдпрд╛ рдпрд╛рддрд▓рд╛ рдмрджрд▓ timetable рдЪреЗ рдирд┐рдпрдо рдореЛрдбрддреЛ. Layer рдЪрд╛ рдирд┐рдпрдо рдЗрддрдХрд╛ рд╕реЛрдкрд╛ рдЖрд╣реЗ рдХреА machine рдкреНрд░рддреНрдпреЗрдХ push рд╡рд░ рдкреНрд░рддреНрдпреЗрдХ import рддрдкрд╛рд╕реВ рд╢рдХрддреЗ тАФ рдореНрд╣рдгрдЬреЗ рддреБрдореНрд╣реА рдХрд╛рдврд▓реЗрд▓реА рд░рдЪрдирд╛ рд╣реАрдЪ рддреБрдордЪреНрдпрд╛рдХрдбрдЪреА рд░рдЪрдирд╛ рд░рд╛рд╣рддреЗ.

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

arch/analyze.py рдордзреАрд▓ layer_violations(g, order, strict) рдкреНрд░рддреНрдпреЗрдХ рдордЬрд▓реНрдпрд╛рд▓рд╛ order рдордзреВрди рдХреНрд░рдорд╛рдВрдХ рджреЗрддреЛ (рд╡рд░рдЪрд╛ рдЖрдзреА; default FLOORS = ["web", "app", "domain", "infra"]). рджреЛрди рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдордЬрд▓реНрдпрд╛рдВрдордзреАрд▓ рдкреНрд░рддреНрдпреЗрдХ рдорд╛рд░реНрдЧрд┐рдХреЗрд╕рд╛рдареА: рдЦрд╛рд▓рдЪрд╛ рдордЬрд▓рд╛ рд╡рд░рдЪреНрдпрд╛рд▓рд╛ import рдХрд░рдд рдЕрд╕реЗрд▓ рддрд░ рддреА upward; strict=True рдЕрд╕рддрд╛рдирд╛ рдПрдХрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдордЬрд▓реЗ рдЦрд╛рд▓реА рдЬрд╛рдгреЗ рдореНрд╣рдгрдЬреЗ skips a floor. рдХреЛрдгрддреНрдпрд╛рд╣реА рдордЬрд▓реНрдпрд╛рд╡рд░ рдирд╕рд▓реЗрд▓реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛ (main, composition root) рджреБрд░реНрд▓рдХреНрд╖рд┐рдд рдХреЗрд▓реНрдпрд╛ рдЬрд╛рддрд╛рдд тАФ рд╕рдЧрд│реНрдпрд╛рдВрдирд╛ рдУрд│рдЦрдгреНрдпрд╛рдЪреА рдкрд░рд╡рд╛рдирдЧреА рдЕрд╕рд▓реЗрд▓реА рддреА рдПрдХрдореЗрд╡ рдЦреЛрд▓реА рдЖрд╣реЗ.

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

python3 arch/demo.py layers
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, layer_violations
g = scan().imports
for order in (["web", "app", "domain", "infra"], ["web", "app", "infra", "domain"], ["web", "infra", "app", "domain"]):
    for strict in (False, True):
        v = layer_violations(g, order, strict)
        print(f"{' тЖТ '.join(order):<30} {'strict ' if strict else 'relaxed'} {len(v)} violation(s)")
EOF

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

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

тФАтФА floors, top to bottom: web тЖТ app тЖТ domain тЖТ infra ┬╖ a room may import its own floor or any floor BELOW
   relaxed layering: 4 violations
     domain.timetable тЖТ web.format (upward)
     infra.db тЖТ domain.grades (upward)
     infra.db тЖТ domain.ports (upward)
     infra.email тЖТ domain.ports (upward)
   strict layering (only the floor directly below): 5 violations тАФ adds app.enrol тЖТ infra.email (skips a floor)

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

web тЖТ app тЖТ domain тЖТ infra     relaxed 4 violation(s)
web тЖТ app тЖТ domain тЖТ infra     strict  5 violation(s)
web тЖТ app тЖТ infra тЖТ domain     relaxed 2 violation(s)
web тЖТ app тЖТ infra тЖТ domain     strict  8 violation(s)
web тЖТ infra тЖТ app тЖТ domain     relaxed 3 violation(s)
web тЖТ infra тЖТ app тЖТ domain     strict  9 violation(s)

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

рдкрд╛рд░рдВрдкрд░рд┐рдХ рдХреНрд░рдорд╛рдиреЗ (database рд╕рдЧрд│реНрдпрд╛рдд рдЦрд╛рд▓реА) рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ 4 рд╡рд░ рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдорд╛рд░реНрдЧрд┐рдХрд╛ рд╕рд╛рдкрдбрддрд╛рдд тАФ рдкрдг рддреНрдпрд╛ рдиреАрдЯ рдкрд╛рд╣рд╛. рддреНрдпрд╛рддрд▓реНрдпрд╛ рддреАрди рдореНрд╣рдгрдЬреЗ infra.db рдЖрдгрд┐ infra.email, рдЬреЗ рддреНрдпрд╛рдВрдирд╛ implement рдХрд░рд╛рдпрдЪреЗ рдЕрд╕рд▓реЗрд▓реЗ ports рдЖрдгрд┐ рдкрд░рдд рджреНрдпрд╛рдпрдЪрд╛ рдЕрд╕рд▓реЗрд▓рд╛ Mark class import рдХрд░рддрд╛рдд. рд╣рд╛ рдЧреЛрдВрдзрд│ рдирд╛рд╣реА; рд╣рд╛ adapter рдЖрдкрд▓реЗ рдХрд╛рдо рдХрд░рддреЛ рдЖрд╣реЗ. рдкрд╛рд░рдВрдкрд░рд┐рдХ рд░рдЪрдирд╛ domain рд▓рд╛ database рдЪреНрдпрд╛ рд╡рд░ рдмрд╕рд╛рдпрд▓рд╛ рднрд╛рдЧ рдкрд╛рдбрддреЗ, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ adapter "рдЪреБрдХреАрдЪрд╛" рджрд┐рд╕рддреЛ. Domain рд╕рдЧрд│реНрдпрд╛рдд рдЦрд╛рд▓реА рдареЗрд╡рд╛ (web тЖТ app тЖТ infra тЖТ domain) рдЖрдгрд┐ relaxed violations 2 рд╡рд░ рдпреЗрддрд╛рдд тАФ рдЦрд░реЛрдЦрд░ рдЪреБрдХреАрдЪреНрдпрд╛ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рджреЛрди рдорд╛рд░реНрдЧрд┐рдХрд╛ (domain.timetable тЖТ web.format рдЖрдгрд┐ domain.grades тЖТ infra.db). рдордЧ strict layering 8 рдЖрдгрд┐ 9 рд╡рд░ рдЙрдбреА рдорд╛рд░рддреЗ: domain рд╕рдЧрд│реНрдпрд╛рдд рдЦрд╛рд▓реА рдЕрд╕рд▓реНрдпрд╛рдореБрд│реЗ рдкреНрд░рддреНрдпреЗрдХ use case рд▓рд╛ рддрд┐рдереЗ рдкреЛрд╣реЛрдЪрдгреНрдпрд╛рд╕рд╛рдареА infra рдордЬрд▓рд╛ "рд╡рдЧрд│рд╛рд╡рд╛" рд▓рд╛рдЧрддреЛ. Core рддрд│рд╛рд╢реА рдЧреЗрд▓рд╛ рдХреА strict рд░рдЪрдирд╛ рдиреАрдЯ рдмрд╕рдд рдирд╛рд╣реА. рдордЬрд▓реНрдпрд╛рдВрдЪрд╛ рдХреНрд░рдо рд╣рд╛рдЪ design рдирд┐рд░реНрдгрдп рдЖрд╣реЗ.

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

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

On a real project тАФ import-linter (Python) рдкреНрд░рддреНрдпреЗрдХ run рд╡рд░ layers contract рддрдкрд╛рд╕рддреЛ. .importlinter рдордзреНрдпреЗ (рдХрд┐рдВрд╡рд╛ pyproject.toml рдордзреАрд▓ [tool.importlinter] table рдордзреНрдпреЗ):

[importlinter]
root_package = schoolapp

[importlinter:contract:floors]
name = Floors only use the floors below
type = layers
layers =
    schoolapp.web
    schoolapp.app
    schoolapp.domain
    schoolapp.infra
pip install import-linter
lint-imports          # exits non-zero and lists each illegal import chain

ArchUnit (Java) рд╣реЗрдЪ рдПрдХрд╛ unit test рдордзреНрдпреЗ рд╕рд╛рдВрдЧрддреЛ:

@ArchTest
static final ArchRule floors = layeredArchitecture().consideringAllDependencies()
    .layer("Web").definedBy("..web..")
    .layer("App").definedBy("..app..")
    .layer("Domain").definedBy("..domain..")
    .layer("Infra").definedBy("..infra..")
    .whereLayer("Web").mayNotBeAccessedByAnyLayer()
    .whereLayer("App").mayOnlyBeAccessedByLayers("Web")
    .whereLayer("Domain").mayOnlyBeAccessedByLayers("App", "Web")
    .whereLayer("Infra").mayOnlyBeAccessedByLayers("Domain", "App", "Web");

JavaScript рдЖрдгрд┐ TypeScript рд╕рд╛рдареА dependency-cruiser рд╣реЗ forbidden рдирд┐рдпрдорд╛рдВрдиреА рдХрд░рддреЛ; .NET рд╕рд╛рдареА NetArchTest.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдордЬрд▓реНрдпрд╛рдВрдЪрд╛ рдХреНрд░рдо рд╣реЗрддреБрдкреВрд░реНрд╡рдХ рдирд┐рд╡рдбрд╛ тАФ рдЖрдгрд┐ database рдХреБрдареЗ рдареЗрд╡рддрд╛ рддреЗ рд▓рдХреНрд╖рд╛рдд рдШреНрдпрд╛. Adapters рд╕рддрдд violations рдореНрд╣рдгреВрди рджрд┐рд╕рдд рдЕрд╕рддреАрд▓, рддрд░ рддреБрдордЪрд╛ рдирд┐рдпрдо рддреБрдордЪреНрдпрд╛ design рд╢реА рд▓рдврддреЛ рдЖрд╣реЗ. рдкреБрдврдЪрд╛ рдзрдбрд╛ рдЗрдорд╛рд░рдд рдЖрддреВрди рдмрд╛рд╣реЗрд░ рдЙрд▓рдЯрд╡рддреЛ.

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

рдирд┐рдпрдордкреБрд╕реНрддрд┐рдХрд╛ рдордЬрд▓рд╛рдЪ рдирд╕реВрди рдЗрдорд╛рд░рддреАрдЪрд╛ рдордзреНрдпрднрд╛рдЧ рдЕрд╕рддреА, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рджрд╛рд░ рдмрд╛рд╣реЗрд░рдЪреНрдпрд╛ рджрд┐рд╢реЗрдиреЗ рдЙрдШрдбрдд рдЕрд╕рддреЗ, рддрд░? Ports and adapters.

git checkout lesson-04-hexagonal

ЁЯПв Lesson 03 тАФ Layers: floors, and which way the stairs go

ЁЯУН You are here: Lesson 03 of 12 ┬╖ Previous: lesson-02-modularity ┬╖ Next: lesson-04-hexagonal


ЁЯУж What's in this branch

Lessons 01тАУ02, plus a rule. The classic layered architecture stacks the code in floors тАФ web (presentation) on top, then app (use cases), then domain (business rules), then infra (database, email) at the bottom тАФ with one rule: a floor may only use the floors below it. How to detect layer violations automatically, strict vs relaxed layering, and why this lesson ends with a surprise. layer_violations() in arch/analyze.py and layers() in arch/demo.py.

ЁЯзТ Explain like I'm 5

The school building has four floors. ЁЯПв

The rule: you may go down the stairs, never up. The office may look in the rule book. The rule book may not ring the reception to ask how to print a day name.

Aishwarya walks every corridor with a clipboard and writes down each one that goes up the stairs. That list is the layer violations.

ЁЯЧ║я╕П Diagram

flowchart TB
    web["ЁЯЦея╕П web тАФ reception (top floor)"]
    app["ЁЯзСтАНЁЯПл app тАФ the office"]
    domain["ЁЯУЪ domain тАФ the rule book"]
    infra["ЁЯФМ infra тАФ store room + post room (ground floor)"]
    web -->|"ok: down"| app
    app -->|"ok: down"| domain
    domain -->|"ok: down"| infra
    domain -.->|"тЭМ upward: timetable тЖТ web.format"| web
    infra -.->|"тЭМ upward ├Ч3: db тЖТ grades, db тЖТ ports, email тЖТ ports"| domain
    app -.->|"strict only: skips a floor (enrol тЖТ email)"| infra

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

тЭУ What

ЁЯдФ Why

Because a floor plan with stairs in both directions has no top and no bottom: you cannot replace the reception without checking the store room, and a change to how dates are printed breaks the timetable rules. A layer rule is simple enough that a machine can check every import on every push тАФ so the structure you drew stays the structure you have.

ЁЯФз How (in this repo)

layer_violations(g, order, strict) in arch/analyze.py gives each floor a rank from order (top first; the default is FLOORS = ["web", "app", "domain", "infra"]). For each corridor between two different floors: a lower floor importing a higher one is upward; with strict=True, going down more than one floor is skips a floor. Rooms not on any floor (main, the composition root) are ignored тАФ it is the one room allowed to know everyone.

ЁЯзк Try it

python3 arch/demo.py layers
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, layer_violations
g = scan().imports
for order in (["web", "app", "domain", "infra"], ["web", "app", "infra", "domain"], ["web", "infra", "app", "domain"]):
    for strict in (False, True):
        v = layer_violations(g, order, strict)
        print(f"{' тЖТ '.join(order):<30} {'strict ' if strict else 'relaxed'} {len(v)} violation(s)")
EOF

тЬЕ Verify тАФ what you should see

layers prints:

тФАтФА floors, top to bottom: web тЖТ app тЖТ domain тЖТ infra ┬╖ a room may import its own floor or any floor BELOW
   relaxed layering: 4 violations
     domain.timetable тЖТ web.format (upward)
     infra.db тЖТ domain.grades (upward)
     infra.db тЖТ domain.ports (upward)
     infra.email тЖТ domain.ports (upward)
   strict layering (only the floor directly below): 5 violations тАФ adds app.enrol тЖТ infra.email (skips a floor)

Your snippet prints:

web тЖТ app тЖТ domain тЖТ infra     relaxed 4 violation(s)
web тЖТ app тЖТ domain тЖТ infra     strict  5 violation(s)
web тЖТ app тЖТ infra тЖТ domain     relaxed 2 violation(s)
web тЖТ app тЖТ infra тЖТ domain     strict  8 violation(s)
web тЖТ infra тЖТ app тЖТ domain     relaxed 3 violation(s)
web тЖТ infra тЖТ app тЖТ domain     strict  9 violation(s)

ЁЯПБ What you just proved

With the classic order (database at the bottom), Aishwarya finds 4 upward corridors тАФ but look at them. Three are infra.db and infra.email importing the ports and the Mark class they need to implement and return. That is not a mess; that is an adapter doing its job. The classic stack forces the domain to sit on top of the database, so every adapter looks "wrong". Put the domain at the bottom (web тЖТ app тЖТ infra тЖТ domain) and relaxed violations drop to 2 тАФ the two corridors that really are wrong (domain.timetable тЖТ web.format and domain.grades тЖТ infra.db). Strict layering then jumps to 8 and 9: with the domain at the bottom, every use case must "skip" the infra floor to reach it. A strict stack fits badly once the core is at the bottom. The order of the floors is the design decision.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ import-linter (Python) checks a layers contract on every run. In .importlinter (or the [tool.importlinter] table in pyproject.toml):

[importlinter]
root_package = schoolapp

[importlinter:contract:floors]
name = Floors only use the floors below
type = layers
layers =
    schoolapp.web
    schoolapp.app
    schoolapp.domain
    schoolapp.infra
pip install import-linter
lint-imports          # exits non-zero and lists each illegal import chain

ArchUnit (Java) says the same in a unit test:

@ArchTest
static final ArchRule floors = layeredArchitecture().consideringAllDependencies()
    .layer("Web").definedBy("..web..")
    .layer("App").definedBy("..app..")
    .layer("Domain").definedBy("..domain..")
    .layer("Infra").definedBy("..infra..")
    .whereLayer("Web").mayNotBeAccessedByAnyLayer()
    .whereLayer("App").mayOnlyBeAccessedByLayers("Web")
    .whereLayer("Domain").mayOnlyBeAccessedByLayers("App", "Web")
    .whereLayer("Infra").mayOnlyBeAccessedByLayers("Domain", "App", "Web");

For JavaScript and TypeScript, dependency-cruiser does it with forbidden rules; for .NET, NetArchTest.

ЁЯПн Why this matters in production: pick the floor order on purpose тАФ and notice where you put the database. If adapters keep showing up as violations, your rule is fighting your design. The next lesson turns the building inside out.

тПня╕П Next

What if the rule book were not a floor at all, but the middle of the building, with every door facing out? Ports and adapters.

git checkout lesson-04-hexagonal
тЖР PreviousmodularityNext тЖТhexagonal

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