code-architecture

Architecture patterns and code organization that survive contact with reality. The goal is code you can still understand, test, and change 18 months after it was written. This covers the decisions made at the module and application level — not how to split services, not how to design schemas, but how to organize the code within a single deployable unit.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "code-architecture" with this command: npx skills add erichowens/some_claude_skills/erichowens-some-claude-skills-code-architecture

Code Architecture

Architecture patterns and code organization that survive contact with reality. The goal is code you can still understand, test, and change 18 months after it was written. This covers the decisions made at the module and application level — not how to split services, not how to design schemas, but how to organize the code within a single deployable unit.

When to Use

Use for:

  • Choosing an architecture pattern for a new project (clean, hexagonal, feature-based, vertical slice)

  • Diagnosing architectural problems in existing code (fat controllers, circular deps, tangled business logic)

  • Applying dependency inversion to untangle tightly coupled code

  • Deciding on folder structure and module organization

  • Applying SOLID principles at the module scale (not just class scale)

  • Setting up dependency injection containers

  • Defining module boundaries and barrel exports

  • Designing the test pyramid relative to architecture layers

NOT for:

  • Splitting a monolith into microservices (use microservices-patterns )

  • Database schema design, normalization, query optimization (use database-design-patterns )

  • Framework-specific routing or middleware setup

  • CI/CD pipeline architecture

Core Decision: Which Architecture Pattern?

flowchart TD Start[New project or restructuring?] --> Team{Team size?}

Team -->|Solo / 2 dev| Size{App complexity?}
Team -->|3-8 dev| MedComplex{Domain complexity?}
Team -->|8+ dev| Large[Feature-based / Vertical Slice]

Size -->|Simple CRUD, low domain logic| Simple[Flat + MVC or Feature-Based]
Size -->|Complex domain, long-lived| Hex[Hexagonal Architecture]

MedComplex -->|Low: thin business layer| FeatureBased[Feature-Based Organization]
MedComplex -->|High: rich business rules| Clean[Clean Architecture]
MedComplex -->|Mixed: CRUD + some complex| Vertical[Vertical Slice Architecture]

Simple --> SCheck{Will it grow?}
SCheck -->|No, short-lived| MVC[Layer-based MVC is fine]
SCheck -->|Yes| FeatureBased

Large --> Testing{Testing rigor?}
Testing -->|Low: mostly E2E| Vertical
Testing -->|High: unit + integration| Clean

Clean --> ClNote[Entities → Use Cases → Interface Adapters → Frameworks]
Hex --> HexNote[Domain core ↔ Ports ↔ Adapters]
FeatureBased --> FBNote[features/X contains all layers for X]
Vertical --> VSNote[Each slice owns its full stack top to bottom]

Clean Architecture Layers

The classic onion — each ring depends only inward, never outward.

graph TB subgraph Frameworks ["Frameworks & Drivers (outer)"] DB[(Database)] HTTP[HTTP Server] UI[UI / CLI] External[External APIs] end

subgraph Adapters ["Interface Adapters"]
    Controllers[Controllers / Routes]
    Presenters[Presenters]
    Gateways[Repository Implementations]
    DTOs[DTOs / View Models]
end

subgraph UseCases ["Use Cases (Application Layer)"]
    UC1[CreateOrder]
    UC2[ProcessPayment]
    UC3[NotifyUser]
    Ports[Repository Interfaces]
end

subgraph Entities ["Entities (Domain)"]
    Order[Order]
    User[User]
    Payment[Payment]
    Rules[Business Rules]
end

HTTP --> Controllers
Controllers --> UC1
UC1 --> Ports
Gateways --> Ports
DB --> Gateways
UC1 --> Order

Dependency rule: Source code dependencies point inward only. Entities know nothing about use cases. Use cases know nothing about controllers. Controllers know nothing about database drivers.

What belongs where:

Layer Contains Examples

Entities Enterprise business rules Order , Payment , domain events, value objects

Use Cases Application-specific business logic CreateOrderUseCase , RefundPaymentUseCase

Interface Adapters Convert data between use cases and external forms Controllers, Presenters, Repository implementations

Frameworks External tools, databases, UI frameworks Express, Postgres, React

Hexagonal Architecture (Ports and Adapters)

Simpler mental model than clean architecture for many teams: everything connects through ports (interfaces), and adapters implement them.

graph LR subgraph Domain ["Domain (Hexagon)"] Logic[Business Logic] DPorts[Driven Ports<br/>interfaces the domain calls] end

subgraph Driving ["Driving Adapters&#x3C;br/>(what drives the app)"]
    HTTP2[HTTP Controller]
    CLI2[CLI Command]
    Test[Test Harness]
end

subgraph Driven ["Driven Adapters&#x3C;br/>(what the app drives)"]
    DB2[(PostgreSQL Adapter)]
    Email[SendGrid Adapter]
    Cache[Redis Adapter]
end

HTTP2 -->|calls| Logic
CLI2 -->|calls| Logic
Test -->|calls| Logic
Logic -->|calls port| DPorts
DPorts -.->|implemented by| DB2
DPorts -.->|implemented by| Email
DPorts -.->|implemented by| Cache

When hexagonal beats clean: When you need to swap infrastructure easily (test with in-memory, prod with Postgres), or when your business logic is rich enough to warrant isolation but doesn't need the full layer separation of clean architecture.

Feature-Based vs Layer-Based Organization

Layer-Based (Traditional MVC)

src/ controllers/ user.controller.ts order.controller.ts payment.controller.ts services/ user.service.ts order.service.ts payment.service.ts repositories/ user.repository.ts order.repository.ts models/ user.model.ts order.model.ts

Problem: Adding or modifying the "orders" feature requires touching files in four directories. Understanding the orders domain requires context-switching across folders.

Feature-Based (Recommended for Growing Apps)

src/ features/ orders/ orders.controller.ts orders.service.ts orders.repository.ts orders.schema.ts orders.types.ts orders.test.ts index.ts ← public API (barrel export) users/ users.controller.ts users.service.ts ... index.ts payments/ ... shared/ database/ logger/ config/

Benefits: All order-related code is co-located. Deleting a feature is one folder deletion. Onboarding a developer to the orders domain is one directory.

Rule: Features import from shared/ and from each other's public index.ts only. They never reach into each other's internals.

Anti-Pattern: Business Logic in Controllers (Fat Controller)

Novice: "Controllers are where requests come in, so I'll put the logic there too. It's convenient."

// Fat controller — don't do this app.post('/orders', async (req, res) => { const { userId, items } = req.body;

// Business logic #1: calculate total const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);

// Business logic #2: apply discount const user = await db.users.findById(userId); const discount = user.memberSince < oneYearAgo ? 0.1 : 0; const finalTotal = total * (1 - discount);

// Business logic #3: check inventory for (const item of items) { const stock = await db.inventory.findById(item.productId); if (stock.quantity < item.qty) { return res.status(409).json({ error: 'Out of stock' }); } }

// Side effects mixed in const order = await db.orders.create({ userId, items, total: finalTotal }); await emailService.send(user.email, 'order-confirmation', { order }); await inventory.decrement(items);

res.json(order); });

Expert: Controllers are HTTP adapters. They translate HTTP → domain, call a use case or service, then translate domain → HTTP response. All business logic belongs in a use case or domain service that can be tested without an HTTP context.

// Lean controller app.post('/orders', async (req, res) => { try { const command = CreateOrderCommand.fromRequest(req.body); // Validate + map const order = await createOrderUseCase.execute(command); // All logic here res.status(201).json(OrderPresenter.toJSON(order)); // Map to response } catch (error) { errorHandler(res, error); } });

// Use case: testable, framework-agnostic class CreateOrderUseCase { constructor( private readonly orderRepo: OrderRepository, private readonly inventoryService: InventoryService, private readonly discountPolicy: DiscountPolicy, private readonly notifications: NotificationPort, ) {}

async execute(command: CreateOrderCommand): Promise<Order> { const user = await this.orderRepo.findUser(command.userId); const discount = this.discountPolicy.calculate(user); await this.inventoryService.reserveItems(command.items); const order = Order.create(command.items, discount); await this.orderRepo.save(order); await this.notifications.orderCreated(order, user); return order; } }

Detection: Controllers with more than ~20 lines of logic, controllers that import database models directly, controllers with nested if-else business conditions.

Anti-Pattern: Architecture Astronaut (Abstraction for Its Own Sake)

Novice: "I'll add a Repository interface, a Repository implementation, a Service, a ServiceInterface, a Factory to create the Service, an EventBus, and a CQRS command handler. This is enterprise-grade."

Expert: Every abstraction has a cost: more files, more indirection, harder onboarding, more to maintain. Abstractions are investments that pay off when they enable testing, swappability, or code reuse. If you're adding a UserServiceInterface with one implementation that will never change, you've paid the abstraction cost without collecting the benefit.

Ask: "What does this abstraction enable that I couldn't do otherwise?"

  • Repository interface → swap real DB for in-memory in tests. Pays off immediately.

  • Service interface → if there's only ever one service, this is ceremony.

  • Factory pattern → pays off when object creation is complex or has multiple strategies.

  • Event bus → pays off when many components need to react to domain events without knowing about each other.

Detection: Files named *Interface.ts , *Abstract.ts , *Factory.ts that have only one implementer and one caller, and that implementer never changes.

Timeline: Enterprise Java (2005-2015) made abstract-everything the default. Spring Framework encouraged this. The post-2015 Node.js and Go communities pushed back with "boring technology" principles. In 2026, the right level of abstraction is contextual — neither zero nor maximum.

Anti-Pattern: Circular Dependencies

Novice: "The Order module needs to know about Users, and the User module needs to check their orders. So I'll import each from the other."

// orders/order.service.ts import { UserService } from '../users/user.service'; // Order → User

// users/user.service.ts import { OrderService } from '../orders/order.service'; // User → Order

// Node.js will silently give you undefined at runtime // Jest will give you cryptic "Cannot access before initialization" errors

Expert: Circular dependencies indicate a domain modeling problem. Two modules that genuinely need each other should either be merged into one module, or share a third module that both depend on, or communicate via events/interfaces.

Resolution strategies:

  • Merge: If Order and User are truly inseparable, put them in accounts/

  • Extract shared: Create order-summary/ that both can import from

  • Invert with interface: User module defines OrderSummaryPort interface; Orders implements it; User never imports from Orders

  • Event-driven: User reacts to OrderCreated event rather than calling OrderService directly

Detect circular deps

npx madge --circular src/ npx dpdm --circular src/index.ts

ESLint rule (add to .eslintrc)

"import/no-cycle": "error"

Detection: Runtime errors where a module value is undefined at startup, ESLint import/no-cycle violations, madge circular output.

Dependency Inversion in Practice

The D in SOLID: depend on abstractions, not concretions. Applied at module scale:

// Bad: Use case is coupled to Postgres class CreateOrderUseCase { constructor(private readonly db: PostgresConnection) {}

async execute(cmd: CreateOrderCommand) { await this.db.query('INSERT INTO orders ...'); } }

// Good: Use case depends on an interface interface OrderRepository { save(order: Order): Promise<void>; findById(id: OrderId): Promise<Order | null>; }

class CreateOrderUseCase { constructor(private readonly orderRepo: OrderRepository) {}

async execute(cmd: CreateOrderCommand) { const order = Order.create(cmd); await this.orderRepo.save(order); // No SQL, no Postgres, no coupling } }

// Production: Postgres implements the interface class PostgresOrderRepository implements OrderRepository { async save(order: Order): Promise<void> { /* Postgres SQL / } async findById(id: OrderId): Promise<Order | null> { / Postgres SQL */ } }

// Tests: in-memory implements the same interface class InMemoryOrderRepository implements OrderRepository { private orders = new Map<string, Order>(); async save(order: Order) { this.orders.set(order.id, order); } async findById(id: OrderId) { return this.orders.get(id) ?? null; } }

When DI containers are worth it: When you have many dependencies that need wiring, and wiring manually becomes error-prone or repetitive. NestJS, InversifyJS (TypeScript), Python's dependency-injector, Spring (Java).

When DI containers are overkill: Simple scripts, small services with few dependencies, serverless functions, Go projects (constructor injection is idiomatic and sufficient).

Module Boundaries and Barrel Exports

Each feature/module should expose a public API via index.ts :

// features/orders/index.ts — public API export { CreateOrderUseCase } from './create-order.use-case'; export { OrderRepository } from './order.repository.interface'; export type { Order, OrderStatus } from './order.entity'; // NOT exported: internal helpers, SQL queries, implementation details

// Other modules import from the public API only import { CreateOrderUseCase } from '@/features/orders'; // NOT: import from '@/features/orders/create-order.use-case'

Enforce with ESLint:

// .eslintrc { "rules": { "import/no-internal-modules": ["error", { "allow": ["/*.test.ts", "/index.ts"] }] } }

Testing Architecture (Test Pyramid Placement)

Each architecture layer has a natural test type:

Layer Test Type Speed Coverage

Entities / Domain Unit tests Instant 90%+

Use Cases / Application Unit tests with mocks Fast 80%+

Interface Adapters Integration tests Medium 70%+

Frameworks / External E2E / contract tests Slow Key paths

Rule: Business logic tests should not require starting a server, connecting to a database, or making network calls. If your use case tests require a real database, your architecture has leaked infrastructure concerns into the domain.

References

  • references/architecture-patterns.md — Consult for detailed patterns: clean architecture layers, hexagonal ports and adapters, vertical slice, feature-based organization with worked examples

  • references/dependency-inversion.md — Consult for IoC containers in TypeScript and Python, constructor injection patterns, when DI is overkill, common DI anti-patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

bot-developer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-actions-pipeline-builder

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cloudflare-worker-dev

No summary provided by upstream source.

Repository SourceNeeds Review