UNPKG

create-hokage-js-app

Version:

🔥 Best CLI tool to create a MERN stack template. Quick, clean, and customizable.

282 lines (187 loc) • 11.4 kB
# Architecture Rules Hard rules for system structure, module boundaries, and dependency direction. Agents must apply these before introducing new packages, services, or cross-cutting changes. Also read: - `oop.md` — OOP / SOLID / rich domain (enforceable) - `../architecture/patterns.md` — when to apply design patterns instead of ad hoc procedural logic --- ## Dependency Direction (Non-Negotiable) Dependencies point inward toward domain logic. Outer layers may depend on inner layers. Inner layers must never depend on outer layers. ``` UI / Controllers / Gateways ↓ Application Services / Use Cases ↓ Domain Models / Domain Services ↓ (no outbound deps to infrastructure) ``` Infrastructure (DB, HTTP clients, queues, filesystem) is injected into application/domain via interfaces defined in the inner layers. **Why:** Reversing this creates untestable code and locks business rules to frameworks. **Checklist:** - [ ] Domain modules do not import HTTP, ORM entity annotations used as domain, or framework routers - [ ] Application services do not import controller/request types - [ ] Repositories implement interfaces declared in domain or application, not the reverse --- ## Clean Architecture ### Layers | Layer | Allowed contents | Forbidden | |-------|------------------|-----------| | Domain | Entities, value objects, domain services, domain events, repository *interfaces* | Framework imports, SQL, HTTP | | Application | Use cases / interactors, DTOs for use-case I/O, orchestration, transaction boundaries | UI widgets, raw SQL in use cases | | Interface adapters | Controllers, presenters, gateway adapters, mappers | Business rules | | Infrastructure | ORM, HTTP clients, message brokers, file storage, clock, UUID generators | Business decision logic | ### Rules - Controllers translate HTTP ↔ application DTOs. They must not contain business rules (no pricing, eligibility, permission policy beyond calling an auth service). - Use cases own one business operation (e.g. `PlaceOrder`, `CancelSubscription`). Prefer one public method per use-case class when the operation is non-trivial. - Domain entities encapsulate invariants. Invalid states must be unrepresentable (constructors/factories reject bad data). - Map persistence models ↔ domain models at the repository boundary. Do not leak ORM entities into controllers or UI. **Why:** Keeps business rules portable and unit-testable without a database or web server. --- ## Hexagonal Architecture (Ports & Adapters) - **Ports** are interfaces defining what the app needs (outbound) or offers (inbound). - **Adapters** implement ports for specific technologies (Postgres repository, Stripe payment gateway, REST controller). - Name ports by capability, not technology: `PaymentGateway`, not `StripeClient` (Stripe is an adapter name). - Application core depends only on ports. Wire adapters in composition root / DI container. **Example:** ```text Port: OrderRepository.save(order) Adapter: PostgresOrderRepository.save(order) // SQL lives here only ``` **Checklist:** - [ ] New external system = new adapter behind an existing or new port - [ ] No direct SDK calls from use cases or domain --- ## Layered Architecture When the project uses classic layers (`controller → service → repository`): - Controllers: routing, status codes, request validation wiring, response mapping - Services: business orchestration and rules - Repositories: persistence only (queries, inserts, updates, deletes) - Services must not import HTTP request/response types - Repositories must not call other services or emit HTTP - No skipping layers (controller must not call repository directly unless the project already documents an exception for trivial CRUD read models) **Why:** Skipping layers duplicates persistence rules and breaks transaction ownership. --- ## Feature-First Architecture Organize by business capability when the codebase is product-facing and features evolve independently: ```text features/ billing/ api/ application/ domain/ infrastructure/ identity/ catalog/ ``` Rules: - Cross-feature imports go through a public API module of the feature (`features/billing/index` or `public/`), not deep internal paths. - Shared kernel holds truly shared primitives (Result, Money, clock). Do not dump feature code into `shared/` to avoid thinking about ownership. - Prefer duplicating a 10-line helper inside a feature over a premature shared abstraction used by one feature. **Why:** Feature boundaries reduce merge conflicts and make ownership clear. --- ## Modular Monolith Default starting architecture for most products. - Modules have explicit public APIs and private internals. - Modules communicate via: 1. Synchronous calls to public interfaces, or 2. Domain/integration events (async) - No shared database tables across modules without an owned integration table or ACL. Prefer each module owns its tables. - Extract a microservice only when there is a proven need: independent scaling, separate deploy cadence, or strong team/ownership boundary — not because “microservices are modern.” **Why:** Distributed systems tax is high; modular monolith keeps deploy simplicity with clear seams for later extraction. --- ## Microservice Boundaries Split a service only when **all** of the following hold: 1. Independent deployability is required 2. Data ownership can be split without distributed transactions for the main flows 3. Team or SLO boundaries justify the operational cost Rules after split: - No shared mutable database between services - Sync calls: timeouts, retries with idempotency, circuit breakers - Prefer eventual consistency via events for cross-service workflows - Define an anti-corruption layer when consuming another service’s model - Version public APIs; never break consumers without a migration window **Anti-pattern:** “Nanoservices” that need 2PC or chatty sync calls for every user action. --- ## Dependency Injection - Construct the object graph in one composition root (main, app factory, DI module). - Prefer constructor injection. Avoid service locators in application code. - Inject interfaces/ports, not concrete infrastructure types, at application boundaries. - Do not pass the entire DI container into business classes. - Singletons must be thread-safe and free of request-scoped mutable state. **Why:** Hidden global dependencies make tests and reasoning impossible. --- ## SOLID (Actionable Form) ### Single Responsibility - A class has one reason to change. If a PR description lists two unrelated motivations for changing the same class, split it. - Controllers handle transport. Services handle use cases. Repositories handle persistence. ### Open/Closed - Extend behavior via new classes/strategies/handlers rather than editing large switch statements when adding a third+ variant. - Prefer strategy/plugin registration for payment methods, notification channels, exporters. ### Liskov Substitution - Subtypes must honor the parent contract. Do not override methods to throw `UnsupportedOperation` for core interface methods — split the interface instead. ### Interface Segregation - Clients must not depend on methods they do not use. Split fat repository interfaces (`UserReader` / `UserWriter`) when read and write callers diverge. ### Dependency Inversion - High-level modules depend on abstractions. Domain defines repository interfaces; infrastructure implements them. --- ## DRY / KISS / YAGNI | Principle | Rule | Limit | |-----------|------|-------| | DRY | Extract duplication only after the same logic appears in **3** places **or** two places with identical change reasons | Do not create shared utils for speculative reuse | | KISS | Prefer the simplest design that passes tests and meets SLOs | No framework for a single if/else | | YAGNI | Do not build extension points, config flags, or abstractions for unrequested futures | Delete unused code paths in the same PR that proves they are dead | **Never create duplicate utilities.** Before adding `utils/formatDate`, search the repo for existing date helpers. --- ## CQRS (When Appropriate) Use CQRS when read models and write models diverge enough that one model causes pain (complex reporting, high read QPS, different consistency needs). Rules: - Commands mutate state and return acknowledgment / IDs, not large read graphs - Queries never mutate state - Do not introduce separate databases/buses for CQRS until metrics justify it; start with separate query methods/handlers in-process - Keep command-side invariants in the write model; denormalize read models intentionally **When not to use:** Simple CRUD apps. CQRS without need doubles surface area. --- ## Event-Driven Design - Name events as past-tense domain facts: `OrderPlaced`, `PaymentCaptured` - Events carry enough data for consumers to act without synchronous callbacks when practical - Publishers must not assume which consumers exist - Consumers must be idempotent (at-least-once delivery is the default) - Include `eventId`, `occurredAt`, and aggregate `version` or causal metadata - Outbox pattern for reliable publish after DB commit — do not publish then commit (dual-write hazard) **Why:** Dual-write causes lost events or phantom events under failure. --- ## Repository Pattern - Repositories speak in domain aggregates/entities, not rows - Methods named after domain intent: `findActiveByCustomerId`, not `queryUsersJoinOrdersWhere` - No business rules inside repositories (no “if overdue then …”) - Pagination and filtering parameters are explicit; never return unbounded collections from production APIs - One repository per aggregate root unless the project already uses a coarser convention --- ## Service Pattern - Application/domain services orchestrate; they do not become “god” classes - Split services when a file exceeds ~400 lines or when unrelated use cases share a class only for convenience - Domain services hold logic that does not naturally belong to a single entity - Application services own transactions and authorization checks for the use case --- ## DTO Pattern - Use DTOs at trust boundaries: HTTP in/out, message payloads, external APIs - Do not expose domain entities directly as API responses when they contain internal fields or lazy-load proxies - Mapping lives in adapters/mappers, not in domain entities - Validation annotations/schemas apply to input DTOs; domain still enforces invariants --- ## Architecture Decision Records (ADRs) For decisions that affect boundaries, data ownership, or major tech choices: 1. Create an ADR with context, decision, consequences 2. Prefer short ADRs over Slack archaeology 3. Link the ADR from README or `docs/adr/` Agents: if introducing a new architectural pattern (CQRS, new service, event bus), draft or update an ADR in the same change set when the repo uses ADRs. --- ## Architecture Review Checklist - [ ] Dependency direction is inward-only - [ ] New code lives in the correct layer/feature module - [ ] No framework types in domain - [ ] External I/O behind ports with timeouts - [ ] No cross-module deep imports - [ ] No speculative microservice or shared util extraction - [ ] Transactions and consistency model are explicit - [ ] Backwards compatibility considered for API/events/schema