Module Boundaries
1. Boundary Philosophy
TestoQA uses a layered module architecture to keep the codebase predictable and safe:
- Security-sensitive logic (tenant + auth) happens at boundaries
- Business logic lives in domain services
- Data access is isolated in repositories
- The UI renders data and triggers actions, but never enforces security
This structure is designed to:
- reduce coupling,
- prevent “framework creep” into domain logic, and
- make tenant isolation and authorization easier to enforce consistently.
2. What is a “module” in TestoQA?
A module is a cohesive unit of functionality (feature/domain area) with a consistent internal structure.
Typical examples:
- test-cases
- test-runs
- reports
- uploads
- project/membership management
A module is not necessarily a deployable unit — this is a monolith. The module boundary is for code organization and dependency control.
3. Standard Module Structure
Each module follows the same internal layering:
-
Schema
- Validation contracts (e.g., Zod schemas)
- Defines boundary inputs and safe payload shapes
-
DTOs
- Explicit data shapes passed between layers
- Avoid leaking persistence models outward
-
Repository
- Database access only (Prisma)
- No business rules
- Tenant scoping enforced structurally
-
Service
- Business rules + workflows
- Orchestrates repositories and integrations
- Framework-agnostic (no Next.js concerns)
-
Boundary Layer (Actions/Handlers)
- Server Actions / Route Handlers
- Validates input, resolves context, authorizes, orchestrates services
The exact folder names are flexible, but the layers and dependency direction are not.
4. Layer Responsibilities
UI (React components)
Responsible for:
- presentation, UX, local state
- calling server actions / route handlers
- handling loading/error states in the UI
Not responsible for:
- authorization enforcement
- tenant scoping
- database access
- business rules
Boundary Layer (Actions/Handlers)
Responsible for:
- input validation (using module schema)
- authentication resolution
- tenant context resolution
- authorization checks
- invoking services and mapping outputs to DTOs
Not responsible for:
- persistence logic (no Prisma usage)
- complex business rules (belongs in services)
Services
Responsible for:
- business rules and workflows
- orchestration across repositories and integrations
- mapping persistence results into DTOs
Not responsible for:
- Next.js primitives (request/response objects, caching directives, headers)
- direct UI concerns
Repositories
Responsible for:
- Prisma queries and persistence-only concerns
- tenant scoping of tenant-owned tables
- pagination primitives and query composition
Not responsible for:
- authorization decisions
- cross-module workflows
- interpreting business rules
5. Allowed Dependency Direction
Dependencies must flow “down” the stack:
UI → Boundary → Service → Repository → Prisma → Database
Allowed imports (typical)
- UI may import:
- types/DTOs
- boundary action callers (not the implementation internals)
- Boundary may import:
- schemas/DTOs
- services
- authorization helpers
- request context resolver
- Services may import:
- repositories
- DTOs
- integration interfaces
- Repositories may import:
- Prisma client
- query helpers
- DTO mapping helpers (if kept persistence-local)
6. Forbidden Dependencies
These are architectural violations:
- UI → Prisma / repositories
- UI → services (directly invoking domain logic without boundary enforcement)
- Repository → services (inversion of control violation)
- Services → Next.js framework primitives (request, cookies, headers, caching directives)
- Cross-module repository calls that bypass the target module’s service layer
- Sharing “internal” module files across modules without an explicit contract
Rule of thumb: If a dependency would bypass tenant resolution or authorization checks, it’s not allowed.
7. Boundary Enforcement Mechanisms
Enforcement is layered:
Structural enforcement
- Consistent module folder structure
- Dedicated boundary entry points (actions/handlers)
- Repositories accept tenant context (or equivalent) as required input
Review enforcement
PRs are reviewed against the rules in this document.
Optional tooling (recommended)
Over time, add:
- lint rules for forbidden imports (UI importing repositories/Prisma)
- module-level “public API” barrel exports
- path aliases that expose only approved surfaces
Tooling is helpful, but the architectural contract is the primary safeguard.
8. Adding a New Module
When introducing a new feature module:
-
Define the module’s domain scope
- What it owns
- What it does not own
-
Create schemas (validation contracts)
- Inputs for create/update operations
- Filter/pagination inputs
-
Define DTOs
- Return types for reads
- Output shapes for UI consumption
-
Implement repositories
- Tenant-scoped queries by default
- No business logic
-
Implement services
- Business rules
- Multi-step workflows
- Integration calls behind interfaces
-
Implement boundaries (actions/handlers)
- Validate
- Resolve RequestContext
- Authorize
- Call services
-
Add tests / checks (where applicable)
- Tenant scoping correctness
- Authorization enforcement for critical operations
9. Review Checklist (Module Boundaries)
For changes inside a module:
- UI does not access Prisma or repositories
- Boundary validates input and resolves tenant context
- Authorization happens before data access
- Services contain business rules, not boundaries
- Repositories contain persistence only
- Dependencies follow the allowed direction
- Cross-module calls go through service-level contracts (not repositories)