For a comprehensive comparison of how different architectural patterns handle encapsulation, see Architectural Patterns Comparison.
Migration in progress.
apps/web (Next.js, port 7803) is being replaced by
apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). The
encapsulation principles here remain valid across both stacks, but treat any
apps/web-specific path as legacy-or-current rather than permanent. See
TanStack Start and Rust Migration.Preventing Cross-Layer Communication (Inside an App)
Even without formal hexagonal layering, an app benefits from clear internal boundaries that prevent tight coupling. The patterns below show how dependency inversion, DTOs, and layered responsibility could be applied; where Tuturuuu already does something equivalent (RLS, package boundaries,internal-api
helpers), that is called out as real.
1. The Dependency Inversion Principle (via Ports)
Illustrative pattern. The ports/adapters file paths below
(
packages/types/src/domain/*, apps/web/src/domain/*,
apps/web/src/infrastructure/*) do not exist in the repo today and are shown
purely to teach the dependency inversion principle. packages/types/src exposes
flat modules (db.ts, index.ts, supabase.ts, sdk.ts, …) with no domain/
directory.- Domain logic is pure and testable without infrastructure
- Can swap implementations (Supabase → Drizzle → Prisma) without changing business logic
- Inversion of control - domain doesn’t depend on infrastructure
- Technology agnostic core business logic
@tuturuuu/supabase wrappers, and
packages/internal-api helpers for boundaries instead of a ports/adapters
layer. Adopt the repository pattern only where a unit of logic genuinely needs to
be storage-agnostic or heavily unit-tested.
2. Strict Data Transfer Objects (DTOs)
The outer API layer of a service communicates with its inner Application layer using plain DTOs. This creates a strong boundary that prevents internal, behavior-rich Domain Models from being exposed to external layers, ensuring the core logic remains fully encapsulated. Architectural Principle: External layers communicate via simple data structures, not rich domain objects.Illustrative pattern. The rich
Workspace domain class and
apps/web/src/domain/models/workspace.ts path are illustrative. Real
apps/web API routes work with plain objects and generated DB row types from
@tuturuuu/types/db, not behavior-rich domain entities. The DTO discipline (keep
HTTP request/response shapes separate from internal models, serialize dates as
ISO strings) is still worth applying.- Encapsulation of domain behavior
- Clear boundaries between layers
- API stability - internal changes don’t break API contract
- Serialization control - DTOs are JSON-friendly
- Versioning - can support multiple DTO versions
3. Explicit Layered Responsibility
The architecture enforces a clear separation of concerns. The Presentation layer handles HTTP, the Application layer orchestrates workflows, and the Domain layer contains pure business logic. This clarity prevents logic from being misplaced and ensures layers only interact through their well-defined public interfaces. Architectural Principle: Each layer has a single, well-defined responsibility and communicates only through defined interfaces. Actualapps/web/src structure (real):
application/, domain/, or infrastructure/ directory. Cross-app
product data flows through the shared Supabase database and
packages/internal-api helpers calling REST /api/v1 routes — not through a
ports/adapters layer.
Illustrative hexagonal structure (what an explicitly layered service could
look like — not present today):
Benefits:
- Clear responsibility per layer
- Easy to reason about where code belongs
- Testability - each layer tested independently
- Maintainability - changes confined to appropriate layer
Preventing Unwanted Cross-Service Communication (Between Apps)
1. Background Tasks as a Decoupling Boundary
When one app needs to kick off work in another context without a synchronous dependency, Tuturuuu uses Trigger.dev v4task() definitions in
packages/trigger/src. A trigger payload acts as a small, explicit contract: the
caller passes only what the task needs, and the task does not reach back into the
caller’s internals.
Architectural Principle: Cross-context work is dispatched via explicit task payloads, not by calling another app’s private functions.
Real example (Trigger.dev v4):
The import path is
@trigger.dev/sdk/v3 even though the installed package is
@trigger.dev/sdk@^4.4.5 — the /v3 subpath is the v4 SDK’s stable entrypoint
for task(). The Trigger.dev v2 APIs referenced in older drafts
(client.defineJob, eventTrigger, io.runTask, trigger.event) do not
exist in this repo.2. (Aspirational) Published Event Contracts as the Sole Interface
In a fully event-driven design, the only way services interact is through the events they publish; internal implementation, database schema, and private functions stay completely hidden. Tuturuuu does not implement this today (apps share a database), but the pattern is the strongest form of cross-service encapsulation and is worth understanding. Architectural Principle: Services communicate only via well-defined event contracts. Aspirational example (expressed in real Trigger.dev v4 syntax):- Complete encapsulation of service internals
- Clear API contract via event schemas
- Versioning support for evolution
- Independent deployment of services
- No direct dependencies between services
3. (Hypothetical) Broker-Level Access Control (ACLs)
In a broker-based design you would not rely on trust; the broker itself enforces communication boundaries. Using ACLs you could declare that only one service may produceuser.registered events, and only specific downstream services may
consume them.
Architectural Principle: Enforce service boundaries at the infrastructure level, not just by convention.
Hypothetical example (does not reflect repo code):
- Enforced boundaries at infrastructure level
- Security - services can’t spoof events from other services
- Audit trail - know exactly which services produce/consume events
- Documentation - ACL config documents allowed communication
4. Elimination of Synchronous Coupling
Dispatching background work asynchronously avoids direct, synchronous calls between contexts for slow or non-critical flows. This prevents the tight coupling that arises when one caller must know the network location, API signature, and live availability of another. Tuturuuu applies this with Trigger.dev v4 tasks: the request returns immediately while follow-up work runs out of band. Architectural Principle: Don’t block a user request on slow, non-critical downstream work. Async flow (real Trigger.dev v4 syntax):- No cascading failures
- Independent availability of services
- Faster user responses
- Easier to add new services
Encapsulation in Practice
Workspace Isolation via RLS (real)
This is Tuturuuu’s actual primary encapsulation mechanism: because apps share one Supabase database, Row-Level Security — defined by migrations underapps/database/supabase/migrations/ — keeps every app inside its permitted data
scope at the database level, regardless of which app issues the query.
Package Boundaries (real)
Apps depend on sharedworkspace:* packages and never import another app’s
internals. (Add or update these with scoped installs, e.g.
cd apps/web && bun add <pkg> — never bun add --workspace.)
Summary
Status legend: real = implemented today; illustrative/aspirational = teaching pattern not implemented in the repo.Related Documentation
- Layering Patterns - N-tier vs Hexagonal/Clean architecture comparison
- Hexagonal Architecture - Detailed ports and adapters pattern
- Event-Driven Architecture - Event-based communication
- Microservices Patterns - Service boundaries
- Authorization - RLS policies and permissions