# Architecture Improvement

> Default to a modular monolith with enforced internal boundaries; treat microservices as a destination after boundaries prove stable, not a starting point. Use when structuring a backend, when tempted to split into services, or when module boundaries blur. Not for the actual schema-split / service-extraction migrations (use migration-strategy + schema-design).

- Skill: `jaykim88/architecture-improvement` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/architecture-improvement`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/architecture-improvement/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/architecture-improvement

---


# Architecture Improvement (Modular Monolith Default)

## Purpose
Get the benefits of modularity — clear boundaries, independent reasoning — WITHOUT prematurely paying the microservices tax (network latency, deploy multiplication, distributed debugging). Default to a modular monolith; split only when a boundary has proven stable and a real scale/team driver exists.

**Universal** — the modular-monolith-first decision, enforced internal boundaries, and "extract a service only after the boundary is stable" are architecture principles independent of language.

## Procedure

1. **Default to a modular monolith**
   - One deployable, but internally divided into modules with explicit boundaries
   - Stripe ran a monolith to ~$1B; Shopify runs a modular monolith at massive scale
   - You get modularity's benefits without N deploy pipelines, inter-service latency, and multi-service refactors

2. **Enforce module boundaries inside the monolith**
   - Each module owns its data (no cross-module DB table access); communicate via explicit interfaces
   - Enforce with tooling: package-boundary checks, import linting, or module-system encapsulation (specific tools in Implementation)
   - A boundary that isn't enforced is a boundary that erodes

3. **Decouple modules via events where appropriate**
   - In-process events / Outbox (see `async-messaging`) let modules react without tight coupling
   - This is the seam along which you could later extract a service — if needed

4. **Resist premature microservices — they pay off past a threshold most teams never hit**
   - Costs: infra per service, network latency + failure modes, distributed transactions/tracing, multi-repo refactors, deploy orchestration
   - Fowler's MonolithFirst: most successful microservice systems started as a monolith that grew too big and was then split — and most of those monoliths were *not* cleanly modularized, which is exactly why he prescribes starting monolithic AND building it modular so a later split is viable

5. **Extract a service ONLY when the trigger is real**
   - Independent scaling need (one module's load dwarfs others), independent team ownership at scale, or a different runtime/compliance boundary
   - Extract along an ALREADY-STABLE module boundary — never split a boundary that's still moving

6. **Validate (validation loop)**
   - Check for cross-module data access / circular module deps (the monolith's failure mode); if found, fix the boundary before considering any split
   - Before extracting a service, confirm the boundary hasn't changed in N months (stable) — if it's still churning, don't extract

## Anti-patterns

| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Microservices from day one "for scale" | Modular monolith; split only on a real driver |
| Modules sharing DB tables directly | Each module owns its data; communicate via interfaces |
| Boundaries by convention only | Enforced (package-boundary check / import lint / module system) |
| Extracting a service from a churning boundary | Extract only stable boundaries |
| Distributed monolith (services that must deploy together) | Either truly independent services or one monolith |

## Severity tiers

| Tier | Examples | Action SLA |
|---|---|---|
| **Critical** | Distributed monolith (microservices that can't deploy independently — worst of both worlds); cross-service synchronous chains causing cascading failures | Re-architect; high priority |
| **Major** | Unenforced module boundaries eroding; modules sharing DB tables | Fix this sprint |
| **Minor** | Module naming inconsistency; an event that could decouple a still-acceptable direct call | Schedule within 2 sprints |

## Completion Criteria
- [ ] Default architecture is a modular monolith (unless a documented driver justifies otherwise)
- [ ] Module boundaries enforced by tooling (not convention)
- [ ] No cross-module direct DB access
- [ ] Any service extraction justified by a real driver + a stable boundary (ADR)

## Output
- **Architecture ADR**: `docs/adr/ADR-NNN-modular-monolith.md` — boundaries + enforcement + split criteria
- **Boundary enforcement config**: module/import linting
- **Commit format**: `refactor(arch): enforce <module> boundary` / `docs(adr): modular monolith decision`

## Implementation

### TypeScript + NestJS (default)
- NestJS modules with explicit `imports`/`exports` (encapsulation by default)
- Enforce boundaries: `eslint-plugin-boundaries` / `eslint-plugin-import` `no-restricted-paths`; or Nx module boundaries if in an Nx monorepo
- Module-owned data: each module's services own its tables; cross-module via service interfaces or events (Outbox)
- Extraction seam: a module already communicating via events can become a service with minimal change

### Other stacks
- **Python**: package boundaries + `import-linter`; Django apps as modules
- **Ruby**: Packwerk (Shopify's tool — the reference implementation)
- **Go**: internal packages + `internal/` enforcement; clear package APIs
- **Universal**: modular-monolith-first, enforced boundaries, and extract-stable-boundaries-only are architecture principles, not framework features

## Related skills
- `async-messaging` — events decouple modules within the monolith before any split
- `schema-design` — module boundaries often align with schema ownership
- `migration-strategy` — extracting a service means splitting schema ownership safely

## Reference
- **Key insight encoded**: Default to a modular monolith with enforced internal boundaries; microservices' costs (infra per service, inter-service latency, multi-service refactors) only pay off past a scale threshold most teams never hit. Per Fowler's MonolithFirst, successful microservice systems almost always started as a monolith that outgrew itself — and since most such monoliths were *not* cleanly modularized, the prescription is to start monolithic AND build modular boundaries so a later split is feasible.

