Files
ForgeLM/.agents/rules/architectural-pattern.md

7.5 KiB

trigger
trigger
always_on

Architectural Patterns — Testability-First Design

Core Principle

All code must be independently testable without running the full application or external infrastructure.

Universal Architecture Rules

Rule 1: I/O Isolation

Problem: Tightly coupled I/O makes tests slow, flaky, and environment-dependent.

Solution: Abstract all I/O behind interfaces/contracts:

  • Database queries
  • HTTP calls (to external APIs)
  • File system operations
  • Time/randomness (for determinism)
  • Message queues

Implementation Discovery:

  1. Search the codebase for existing abstraction patterns by looking for symbols named: Interface, Mock, Repository, Store, Adapter (use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)
  2. Match the style (interface in Go, Protocol in Python, interface in TypeScript)
  3. Implement production adapter AND test adapter

Example (Go):


// Contract
type UserStore interface {
  Create(ctx context.Context, user User) error
  GetByEmail(ctx context.Context, email string) (*User, error)
}

// Production adapter
type PostgresUserStore struct { /* ... */ }

// Test adapter
type MockUserStore struct { /* ... */ }

Example (TypeScript/Vue):


// Contract (service layer)
export interface TaskAPI {
  createTask(title: string): Promise<Task>;
  getTasks(): Promise<Task[]>;
}

// Production adapter
export class BackendTaskAPI implements TaskAPI { /* ... */ }

// Test adapter (vi.mock or manual)
export class MockTaskAPI implements TaskAPI { /* ... */ }

Rule 2: Pure Business Logic

Problem: Business rules mixed with I/O are impossible to test without infrastructure.

Solution: Extract calculations, validations, transformations into pure functions:

  • Input → Output, no side effects
  • Deterministic: same input = same output
  • No I/O inside business rules

Examples:


// ✅ Pure function - easy to test
func calculateDiscount(items []Item, coupon Coupon) (float64, error) {
// Pure calculation, returns value
}

// ❌ Impure - database call inside
func calculateDiscount(ctx context.Context, items []Item, coupon Coupon) (float64, error) {
validCoupon, err := db.GetCoupon(ctx, coupon.ID) // NO!
}

Correct approach:


// 1. Fetch dependencies first (in handler/service)
validCoupon, err := store.GetCoupon(ctx, coupon.ID)

// 2. Pass to pure logic
discount, err := calculateDiscount(items, validCoupon)

// 3. Persist result
err = store.SaveOrder(ctx, order)

Rule 3: Dependency Direction

Principle: Dependencies point inward toward business logic.


┌──────────────────────────────────────┐
│  Infrastructure Layer                │
│  (DB, HTTP, Files, External APIs)    │
│                                      │
│  Depends on ↓                        │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│  Contracts/Interfaces Layer          │
│  (Abstract ports - no implementation)│
│                                      │
│  Depends on ↓                        │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│  Business Logic Layer                │
│  (Pure functions, domain rules)      │
│  NO dependencies on infrastructure   │
└──────────────────────────────────────┘

Never:

  • Business logic imports database driver
  • Domain entities import HTTP framework
  • Core calculations import config files

Always:

  • Infrastructure implements interfaces defined by business layer
  • Business logic receives dependencies via injection

Pattern Discovery Protocol

Before implementing ANY feature:

  1. Search existing patterns (MANDATORY): Search the codebase for symbols named: Interface, Repository, Service, Store, Mock (use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)

  2. Examine 3 existing modules for consistency:

  • How do they handle database access?
  • Where are pure functions vs I/O operations?
  • What testing patterns exist?
  1. Document pattern (>80% consistency required):
  • "Following pattern from [task, user, auth] modules"
  • "X/Y modules use interface-based stores"
  • "All tests use [MockStore, vi.mock, TestingPinia] pattern"
  1. If consistency <80%: STOP and report fragmentation to human.

Testability Compliance

These are architectural requirements that the code structure must satisfy — not testing mechanics. A design that cannot meet them is not compliant with this architecture.

Unit testability (non-negotiable):

  • Unit tests MUST run without starting any database, external service, or network call
  • All I/O dependencies MUST be abstractable to the point where a mock can replace them
  • Business logic MUST be exercisable in isolation from infrastructure (enforced by Rules 1 & 2)

Integration testability:

  • Every I/O adapter MUST be independently testable against real infrastructure
  • Adapters must be replaceable — the application must not hard-wire a specific implementation

Test co-location (structural rule):

  • Default: Unit and integration tests co-locate with the implementation they test (test lives next to the code)
  • E2E tests: isolated in a dedicated directory — e2e/ at the project root for single-app projects, or apps/e2e/ for monorepos. They cross feature boundaries and belong to none. See Testing Strategy @testing-strategy.md for the complete E2E directory layout.
  • Language overrides: Some ecosystems have different conventions (e.g., Flutter uses a mirrored test/ directory, Rust uses inline #[cfg(test)] blocks). When a language-specific project structure file exists, its test location rules take precedence over this default. See the relevant project-structure-* file for authoritative guidance.

For test pyramid proportions, naming conventions, tool choices, and language-specific tooling, see Testing Strategy @testing-strategy.md.

Language-Specific Idioms

For language-specific abstraction patterns and test tooling, see the dedicated idiom files listed in code-idioms-and-conventions.md. Those files contain the authoritative guidance per ecosystem and are independently maintainable.

Universal rule: In every language, the testability pattern is the same — I/O is behind an interface, business logic is pure, and tests inject a mock/fake implementation. The idiom files describe how to express this pattern in each ecosystem's syntax.

Enforcement Checklist

Before marking code complete, verify:

  • Can I run unit tests without starting database/external services?
  • Are all I/O operations behind an abstraction?
  • Is business logic pure (no side effects)?
  • Do integration tests exist for all adapters?
  • Does pattern match existing codebase (>80% consistency)?
  • Core Design Principles @core-design-principles.md
  • Testing Strategy @testing-strategy.md
  • Code Organization Principles @code-organization-principles.md
  • Project Structure @project-structure.md
  • Database Design Principles @database-design-principles.md