Skip to Content
🎉 TestoQA 1.0 is released

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:

  1. Schema

    • Validation contracts (e.g., Zod schemas)
    • Defines boundary inputs and safe payload shapes
  2. DTOs

    • Explicit data shapes passed between layers
    • Avoid leaking persistence models outward
  3. Repository

    • Database access only (Prisma)
    • No business rules
    • Tenant scoping enforced structurally
  4. Service

    • Business rules + workflows
    • Orchestrates repositories and integrations
    • Framework-agnostic (no Next.js concerns)
  5. 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.

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:

  1. Define the module’s domain scope

    • What it owns
    • What it does not own
  2. Create schemas (validation contracts)

    • Inputs for create/update operations
    • Filter/pagination inputs
  3. Define DTOs

    • Return types for reads
    • Output shapes for UI consumption
  4. Implement repositories

    • Tenant-scoped queries by default
    • No business logic
  5. Implement services

    • Business rules
    • Multi-step workflows
    • Integration calls behind interfaces
  6. Implement boundaries (actions/handlers)

    • Validate
    • Resolve RequestContext
    • Authorize
    • Call services
  7. 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)
Last updated on