dev-architecture
Source: skills/dev-architecture/SKILL.md
Module boundaries, circular deps, coupling taxonomy, and boundary defenses.
AI 에이전트는 기능을 빠르게 구현하지만 모듈 경계를 의식하지 않는다. 결과적으로 순환 의존성, 암묵적 커플링, barrel 남용이 쌓여 코드베이스가 리팩토링 불가능한 진흙 덩어리가 된다. 이 스킬은 모듈 분할, 의존성 방향, 레이어 경계를 기계적 규칙으로 강제하여 구조적 부패를 차단한다.
LLM은 '가장 가까운 import'를 선호한다. 기능을 구현할 때 이미 아는 파일에서 직접 import하므로, 레이어 경계를 넘는 의존성(UI에서 DB 클라이언트를 직접 import)이 자연스럽게 생긴다. 이건 LLM이 코드를 텍스트 토큰으로 처리하기 때문에 발생하는 구조적 문제다 — LLM은 '이 import가 아키텍처 경계를 넘는다'는 공간적 인식이 없다. madge, pydeps 같은 정적 분석 도구를 CI에 넣어서 기계적으로 잡아야 하는 이유가 여기에 있다.
이 스킬이 해결하는 실제 문제
에이전트가 기능을 추가할 때 가장 가까운 파일에서 import하는 습관이 있다. A가 B를, B가 A를 import하는 순환이 생겨도 에러 없이 동작하다가, 번들 크기 폭발이나 런타임 undefined로 나중에 터진다. ARCH-CYCLE-01은 madge/pydeps로 순환을 CI에서 차단한다.
index.ts에서 모든 것을 re-export하는 barrel 패턴은 편리하지만, 웹팩/롤업의 트리셰이킹을 무력화한다. 하나의 유틸을 import하면 barrel 전체가 번들에 포함된다. barrel-discipline.md는 barrel이 허용되는 조건과 금지 조건을 명시한다.
에이전트는 기존 파일에 계속 코드를 추가하는 경향이 있다. 파일이 400 LOC을 넘으면 여러 책임이 뒤섞여 있을 가능성이 높다. 이 스킬은 기계적 분할 기준(LOC, 의존자 수, 무관 기능 공존)을 제공한다.
Triggers: circular import, module split, layer violation, dependency direction, barrel file, re-export, architecture refactor
Key Concepts
- Structural Decision Gate
- Pre-Change Structural Map
- Layered Architecture Boundaries
- Circular Dependency Detection
- Coupling Taxonomy (8 types)
- Barrel Discipline
Related Skills
Reference Documents
| Document | Description |
|---|---|
| Barrel/Re-export Discipline | barrel/index 파일의 허용 조건과 금지 조건, 트리셰이킹 영향 |
| Circular Dependency Detection & Resolution | 순환 의존성 탐지 명령(madge/pydeps/go vet), 수정 전략, 실제 사례 |
| Implicit Coupling Taxonomy | 8가지 커플링 유형, 심각도 매트릭스, 리팩토링 패턴 |
Sections Overview
| Section | Summary |
|---|---|
| Module Boundaries | 모듈 경계 변경 시 구조 결정을 명시하고 구조 맵을 만든다. |
| Layered Architecture | Presentation/Application/Domain/Infrastructure 4계층 import 규칙. |
| When to Split | 400 LOC 초과, 6+ 직접 의존자, 무관한 기능 공존 시 분할 기준. |
| Dependency Direction | 의존성 역전 원칙과 포트/어댑터 패턴 적용 가이드. |
Academic References
| Paper | Year | Relevance |
|---|---|---|
Untangling Patterns of Two-Class Dependency CyclesarXiv:2306.10599 | 2023 | 38개 OSS 프로젝트에서 순환 의존성을 해소하는 5가지 반복 패턴을 식별. circular-dependencies.md의 수정 전략 근거. |
PeerChecker: Detecting Peer Dependency Resolving Loops in npmarXiv:2505.12676 | 2025 | npm 생태계의 의존성 해결 루프 탐지 도구. 패키지 매니저 수준의 순환 문제 연구. |
Comparing Static and Dynamic Weighted Software Coupling MetricsarXiv:1909.12521 | 2019 | 소스 코드 커플링과 런타임 커플링의 비교 — coupling-taxonomy.md의 8가지 유형 분류 근거. |
Official Guides
- madge — Circular Dependency Detector — JavaScript/TypeScript 순환 의존성 시각화 및 탐지 도구.
- pydeps — Python Module Dependency Visualizer — Python 모듈 의존성 그래프 생성 도구.
Full Specification
Show full SKILL.md content
Dev-Architecture — Module Boundaries & Structural Integrity
> C0/C1 work (small local patches): See dev §0.0 Work Classifier + §0.1 Patch Fast-Path before reading references.
> Always read dev/SKILL.md first for project-wide conventions before applying architecture rules.
Enforces architectural rules that prevent structural decay: circular dependencies, implicit coupling, barrel abuse, and misplaced validation. These rules are mechanical — an AI coding agent can follow them without subjective judgment.
Severity mapping (dev §0.2): Severity: CRITICAL/HIGH ⇒ STRICT; MEDIUM ⇒ DEFAULT.
Modular References
| File | When to Read | What It Covers |
|---|---|---|
references/circular-dependencies.md | Detecting or fixing import cycles | Detection commands (madge/pydeps/go vet), fix strategies, real examples |
references/coupling-taxonomy.md | Reviewing code for hidden coupling | 8 coupling types, severity matrix, refactoring patterns, banned review responses |
references/barrel-discipline.md | Creating/modifying index/barrel files | When barrels OK vs banned, tree-shaking impact, safe barrel template |
External/current architecture evidence
Architecture rules in this skill are local and mechanical. When an architectural
decision depends on current framework guidance, cloud/provider reference
architecture, package deprecation, platform limits, or public source evidence,
read the active search skill and follow its query-rewrite, source-fetch, and
evidence-status rules. Use browser verification only after candidate URLs exist.
---
1. Module Boundaries
Structural Decision Gate
Severity: HIGH
Rule (ARCH-DECISION-01): When changing module boundaries, seams, layering, shared packages, or public exports above C0/C1 local-patch scope, name the structural decision before editing.
Required decision record, inline in the plan or in the repo's ADR/source-of-truth file:
- Context: what pressure forced the boundary change.
- Rejected alternative: at least one plausible option not chosen, with why.
- Chosen move: split, extract, merge, invert dependency, introduce adapter, or change public export.
- Consequences: new dependency direction, public contract impact, migration cost, and follow-up verification.
Route durable, surprising, hard-to-reverse, or cross-team choices to the repo's ADR/current-architecture source of truth instead of leaving them only in chat. dev-scaffolding owns where those durable docs live; this skill owns the boundary decision content.
Pre-Change Structural Map
Severity: HIGH
Rule (ARCH-MAP-01): Before recommending or applying a split/extract/merge for boundary work above C0/C1, produce a compact structural map from code evidence.
Map fields:
- Core modules involved.
- Direct dependents and dependencies.
- Current and intended dependency direction.
- Public boundaries touched (
index.*, package exports, route/API contracts, CLI entry points).
Layered Architecture Boundaries
| Layer | May Import | MUST NOT Import | Example |
|---|---|---|---|
| Presentation (UI/CLI/Controller) | Application, Domain | Infrastructure directly | React component importing DB client |
| Application (Use Cases/Services) | Domain, Ports | Presentation, Infra adapters | Service importing React component |
| Domain (Entities/Value Objects) | Nothing (self-contained) | Any other layer | Entity importing Express |
| Infrastructure (Adapters/DB/HTTP) | Domain (implements ports) | Presentation, Application | DB adapter importing controller |
When to Split a Module
Canonical file-size rule: >400 LOC -> split (DEFAULT). Deviations require a stated reason.
| Signal | Action | |||
|---|---|---|---|---|
| File exceeds 400 LOC | Split by responsibility (DEFAULT) | |||
| Module has 6+ direct dependents | Extract shared interface | |||
| Two unrelated features share a file | Separate into own modules | |||
| Circular import detected | Extract shared types/interfaces to a third module | |||
| Module name contains "and" or "utils" | Split by actual concern |
| 3+ apps/services import the same feature folder | Promote to a monorepo package with its own manifest + public boundary |
| Package needs its own release cadence, CI matrix, or version | Split at package level, not folder level |
Banned Patterns
utils.ts / helpers.ts growing unboundeddate-utils.ts, string-format.ts| Cross-layer direct import | Breaks dependency direction | Use ports/adapters or event bus |
| Shared mutable state between modules | Hidden temporal coupling | Pass explicitly or use event system |
| God module (20+ exports) | Everything depends on it | Extract cohesive sub-modules |
Module SSOT (Single Source of Truth)
Every concept, constant, type, or configuration value MUST have exactly one canonical owner module.
| Concept | Canonical Owner | Consumers Do |
|---|---|---|
| Shared types / interfaces | types/ or contracts/ module | Import, never redefine |
| Constants / magic values | Domain-specific constants module | Import the constant |
| Config / env | Central config module | Import resolved values |
| Validation schemas | Boundary module (API entry) | Import schema, don't recreate |
| API contracts | API layer | Import types from API module |
| Banned | Why | Fix |
|---|---|---|
| Duplicating a type/constant in a consumer | Two sources of truth → drift | Import from canonical owner |
| "Local copy for convenience" | Convenience becomes divergence | Import the original |
| Re-deriving a value that has a canonical source | Silent inconsistency | Import the derived value or computation |
Deep Modules and Seams
Use this vocabulary when deciding whether an abstraction earns its keep:
| Term | Meaning | |||
|---|---|---|---|---|
| Module | A cohesive unit with a named responsibility and public interface | |||
| Interface | The small surface consumers depend on | |||
| Implementation | The hidden work behind that surface | |||
| Depth | Large useful behavior hidden behind a small interface | |||
| Seam | A boundary where alternative implementations are real or likely |
| Adapter | Code translating one external shape into the module's interface |
| Leverage | How much change the abstraction absorbs for its callers |
| Locality | How close related behavior stays to its owning concept |
Frontend depth means small props/events hiding complex rendering, state management,
data transformation, or integration behavior. One adapter usually means hypothetical
indirection; two adapters, or a near-term second adapter, is evidence of a real seam.
Do not expose internals only for tests; test through the public interface or add a
boundary-owned diagnostic hook with production value.
---
2. Circular Dependency Detection & Prevention
Severity: CRITICAL
Rule: No circular dependency may exist between modules. Every detected cycle MUST be resolved before merge.
Required Agent Workflow
| Phase | Required Action | Pass Condition |
|---|---|---|
| 1. Detect | Run ecosystem-specific detection command | Command exits clean (no cycles reported) |
| 2. Classify | Identify cycle type: direct A<->B or transitive A->B->C->A | Type documented |
| 3. Analyze | Determine root cause: shared type? callback? event? | Root interface identified |
| 4. Fix | Apply appropriate fix strategy (see references/) | Detection command passes |
| 5. Verify | Re-run detection + confirm no regressions | Zero cycles in report |
Detection commands are ecosystem-specific. See references/circular-dependencies.md
for command templates, examples, and verification details.
Banned Patterns
| Banned Pattern | Why Banned | Required Fix | ||
|---|---|---|---|---|
| A imports B, B imports A (direct cycle) | Compile failures, bundler issues, test fragility | Extract shared interface to C | ||
Type-only cycle (import type both ways) | Still signals wrong boundary | Move shared types to types/ module | ||
| Barrel re-export creating hidden cycle | Index file masks real dependency graph | Remove barrel, use direct imports | ||
Lazy import to "break" cycle (require() inside function) | Hides the problem, breaks tree-shaking | Fix the architecture, not the symptom | ||
| "It works in runtime" as justification | Fragile, bundler-dependent, blocks refactoring | Must pass static analysis |
| Circular via test file importing source that imports test helper | Test infra leaking into production graph | Isolate test helpers in __test_utils__/ |
Fix Guidance
types.ts or contracts/ module both import| Event producer and consumer import each other | Event bus / mediator pattern |
| Circular at package level (monorepo) | Introduce shared or contracts package |
| UI component imports its container | Lift shared state to context or prop drilling |
| Service layer cycle | Extract orchestrator service or use events |
---
3. Implicit Coupling Taxonomy
Severity: CRITICAL
Rule: Every coupling instance in a code review MUST be classified by type. Coupling severity determines whether the code can merge.
Coupling Types (ordered by severity, worst first)
| # | Type | Definition | Severity | Fix Pattern |
|---|---|---|---|---|
| 1 | Content | Module reaches into another's internals | CRITICAL | Expose via public API/method |
| 2 | Common | Multiple modules share global mutable state | CRITICAL | Dependency injection, immutable config |
| 3 | Control | Module passes flag to control another's logic | HIGH | Polymorphism, strategy pattern |
| 4 | Stamp | Module passes large struct when only one field needed | HIGH | Pass only needed fields |
| 5 | External | Multiple modules depend on same external format | HIGH | Single parser module, shared schema |
| 6 | Temporal | Modules must execute in specific order | MEDIUM | Make ordering explicit (state machine, builder) |
| 7 | Sequential | Output of A is input of B | LOW | Document the contract, validate at boundary |
| 8 | Functional | Modules share a well-defined interface | LOW | This is GOOD coupling — the target state |
See references/coupling-taxonomy.md for examples, detection signals,
refactoring patterns, and banned review responses.
Review Decision Matrix
| Severity | Merge? | Action Required |
|---|---|---|
| CRITICAL (Content, Common) | BLOCK | Must refactor before merge |
| HIGH (Control, Stamp, External) | BLOCK unless justified | Require tech-debt ticket if merged |
| MEDIUM (Temporal) | Allowed with documentation | Add ordering comments or state assertions |
| LOW (Sequential, Functional) | ALLOWED | No action needed |
---
4. Boundary-Only Defensive Programming
Severity: CRITICAL
Rule: Validation and defensive checks belong ONLY at system boundaries. Internal module boundaries MUST trust their callers.
Ownership split: placement (validation happens at the boundary, nowhere else) is owned
by this section; what the validation schema enforces (content/policy) is owned by
dev-security §1.
Validation Location Matrix
| Location | Validate? | Rationale | Example |
|---|---|---|---|
| HTTP/API controller input | YES | Untrusted external data | Zod schema, JSON schema |
| CLI argument parsing | YES | Untrusted user input | yargs/commander validation |
| File system reads | YES | External data, may be corrupt | Parse + validate structure |
| Database query results | YES at ORM-untyped/raw-query boundaries (shape only); NO when a typed schema/ORM guarantees the shape | Untyped results may drift; typed guarantees are trusted (see Banned Patterns) | Check raw-query nulls/shape; trust typed ORM results |
| Message queue consumer | YES | Cross-process boundary | Validate message schema |
| Internal function params | NO | Caller is trusted code you control | Type system handles this |
| Private method args | NO | Same module, same author | Redundant — types suffice |
| Service-to-service in same process | NO | In-process calls share type system | Interface contracts handle this |
Banned Patterns
if (!param) throw at start of every internal function | Redundant with type system, clutters code | Remove — let TypeScript/types enforce |
| Runtime type checks in typed language internals | Duplicates compiler work, adds noise | Trust the type system |
assert(x !== null) in module-internal code | If x can be null, fix the type; if it can't, the assert is noise | Fix type signature or remove assert |
| Validation in domain entity constructor for in-process callers | Entities should be created from validated data | Validate at boundary, trust domain layer |
| Try-catch around every internal call | Hides bugs, makes debugging harder | Let errors propagate, catch at boundary |
| Null checks after DB query that schema guarantees NOT NULL | Distrusts your own schema | Trust schema, validate at migration time |
Allowed Defensive Checks (Exceptions)
| Situation | Why Allowed | Pattern |
|---|---|---|
| Security-critical path (auth, crypto) | Defense in depth required by policy | Double-check even internal calls |
| Data from deserialization (JSON.parse) | Runtime data, types lost | Validate with schema (Zod/io-ts) |
| Plugin/extension boundary | Third-party code, untrusted | Validate at plugin interface |
| Across deployment boundary (microservice call) | Network = system boundary | Full validation required |
| Feature flags / A-B test paths | Runtime variation, not type-safe | Guard with runtime check |
Fix Guidance
| Smell | Diagnosis | Fix |
|---|---|---|
10+ if (!x) throw in one file | Over-defensive internal code | Remove guards, fix types |
| Every function starts with parameter validation | Boundary confusion | Move all validation to entry point |
try { } catch { return null } everywhere | Error suppression | Let errors bubble, handle at boundary |
typeof x === 'string' in TypeScript | Distrusting compiler | Remove, or fix the type to be accurate |
| Same validation in controller AND service | Duplicated boundary | Validate once at controller, service trusts |
---
5. Barrel/Re-export Discipline
Severity: HIGH
Rule: Barrel files (index.ts/index.js/__init__.py) are ONLY allowed at public boundaries — package APIs and feature public boundary exports. Internal convenience barrels are banned.
Barrel Policy Matrix
| Context | Barrel Allowed? | Rationale | |
|---|---|---|---|
Library/package public API (packages/ui/index.ts) | YES | Single entry point for consumers | |
Framework plugin entry (plugin/index.ts) | YES | Plugin contract requires it | |
Feature public boundary export (features/auth/index.ts as the feature's single external entry) | YES | Public Boundary Export (dev-scaffolding §1); external consumers import the boundary | |
| Feature internal convenience barrel (re-exporting siblings for imports inside the feature) | NO | Hides internal structure, breaks tree-shaking | |
Utility folder (utils/index.ts) | NO | Creates coupling magnet |
| Component folder re-exporting siblings | NO | Direct imports are clearer |
Monorepo package boundary (@org/shared/index.ts) | YES | Cross-package contract |
See references/barrel-discipline.md for import examples, tree-shaking
details, ESLint enforcement, and the safe barrel template.
---
6. Review Integration
Architecture Review Checklist (for code-reviewer)
When reviewing any PR that adds/modifies module structure, verify:
- [ ] No new circular dependencies — run
madge --circularor equivalent - [ ] Layer violations — no upward imports (infra->domain OK, domain->infra BLOCKED)
- [ ] Coupling classified — any new cross-module dependency has coupling type identified
- [ ] No CRITICAL/HIGH coupling without justification — Content/Common/Control coupling blocked
- [ ] Barrel files — no new internal barrels; existing public barrels use named exports only
- [ ] Validation placement — new validation is at system boundary, not internal functions
- [ ] Module size — new/modified modules under 400 LOC
- [ ] No "utils" growth — shared code placed in domain-specific module, not catch-all utils
- [ ] Dependency direction — dependencies point inward toward Domain: outer layers depend on inner layers (Presentation/Application/Infrastructure -> Domain), and inner layers never import outward
- [ ] No lazy-import hacks — no
require()inside function body to hide circular deps
Automated Enforcement (CI Recommendations)
| Check | Tool | CI Command | |
|---|---|---|---|
| Layer/dependency rules (preferred CI gate) | dependency-cruiser | npx depcruise --validate .dependency-cruiser.cjs src/ | |
| Import boundaries | eslint-plugin-boundaries | ESLint with boundaries config | |
| Circular deps (quick visualization) | madge | npx madge --circular --extensions ts,tsx src/ && echo "OK" | |
| Barrel abuse | Biome noBarrelFile or ESLint no-restricted-imports | pattern for internal index files | |
| Dead files/exports/deps | knip | npx knip |
| Monorepo package consistency | sherif | npx sherif | |
| Module size | custom script | `find src -name '*.ts' -exec wc -l {} + \ | awk '$1 > 400'` |
Tool roles verified 2026-07-02 (Sources: references/circular-dependencies.md).
---
Cross-Skill References
- Observability: Trace emission at module boundaries is a production/long-lived-runtime concern (DEFAULT there, not universal). See
dev-backend/references/core/observability.mdfor the canonical OTel setup. - Security: Validate at every trust/process/external boundary (HTTP entry, IPC, file/CLI input, third-party responses). Intra-trust-domain module calls follow §4 boundary-only defense — do not re-validate already-trusted data. See
dev-security/SKILL.mdfor input validation and auth patterns.
---
Quick Decision Trees
"Should I create a new module?"
Does the code serve a distinct responsibility?
NO -> Keep in existing module
YES -> Is it used by 3+ other modules?
NO -> Co-locate with primary consumer
YES -> Create dedicated module with clear interface
"Is this coupling acceptable?"
What type? (see taxonomy above)
Content/Common -> BLOCK, refactor now
Control/Stamp/External -> BLOCK unless tech-debt ticket created
Temporal -> ALLOW with documentation
Sequential/Functional -> ALLOW
"Where does this validation go?"
Is the data source external (HTTP, file, queue, DB, user input)?
YES -> Validate here (boundary)
NO -> Is this a security-critical path?
YES -> Validate (defense in depth)
NO -> Trust the type system, no validation needed