Architecture Guidelines
File Organization
- Respect the configured maximum file line limit — large files signal too many responsibilities
- One responsibility per module
- Group by feature or domain, not by file type — reduces cross-directory coupling when a feature changes
BAD: models/, controllers/, views/ (change one feature, touch every directory) GOOD: users/, orders/, payments/ (all feature files colocated)
- Keep related files close together in the directory tree
Modular Monolith First
- Start with a modular monolith — extract microservices only when scaling demands prove the boundary is stable
- Enforce module boundaries at compile time (internal packages, module visibility) — boundaries without enforcement erode
- Use event-driven communication between modules when preparing for future extraction
Architecture Patterns
- Use hexagonal architecture (ports and adapters) for core domains — separate business logic from infrastructure through defined ports
- Consider vertical slice architecture for feature delivery — each use case is self-contained with its own handler, validation, and persistence
- Use CQRS when read and write models have divergent requirements — do not apply it everywhere by default
- Define bounded contexts to identify module boundaries — a module owns its data and exposes only contracts
Dependencies
- Depend on abstractions, not concrete implementations — enables swapping implementations without changing callers
- No circular dependencies between modules — circular deps make code untestable and hard to reason about
- Dependencies flow inward: UI → services → domain → utilities
- Use dependency injection for testability
- Use path aliases (
#src/*,@/*) for cross-module imports — makes dependency direction visible and survives file moves
Design Principles
- Reuse existing components before creating new ones — search the codebase first
- Keep business logic in services or hooks — never in UI components or controllers
- Prefer composition over inheritance — inheritance creates rigid hierarchies that are hard to change
BAD: UserController extends BaseController extends AuthController GOOD: UserController uses AuthService and LoggingMiddleware
- Separate concerns: data access, business rules, presentation
- Design for change: isolate likely change points behind interfaces
Event-Driven Communication
- Use the Outbox Pattern to guarantee at-least-once event delivery without distributed transactions
- Use the Inbox Pattern for message idempotency at the consumer side
- Prefer async events between modules over synchronous calls — reduces coupling and enables independent scaling
API Boundaries
- Define clear contracts between modules
- Use types/interfaces at module boundaries — catches integration errors at compile time
- Validate data at system boundaries, trust internal data
- Keep internal implementation details private
Avoid Over-Engineering
- Solve the current problem, not hypothetical future ones
- Three similar lines are better than a premature abstraction — wait for the pattern to emerge
- Add complexity only when it reduces overall system complexity
- If in doubt, choose the simpler approach