Hexagonal Architecture (Ports and Adapters)
Hexagonal Architecture, also known as Ports and Adapters, is a design pattern for achieving technology independence, testability, and maintainability by isolating core business logic from the technologies that surround it.This page is an educational pattern guide, not a description of Tuturuuu’s
current directory layout. The Tuturuuu apps (
apps/web, apps/finance) are
conventional Next.js App Router apps organized by app/, components/, lib/,
and utils/ — they do not use formal domain/, application/, and
infrastructure/ layers. The code samples below are illustrative; their file
paths are aspirational and do not exist in the repo. Where Tuturuuu genuinely
borrows an idea from this pattern, the text says so explicitly.Architecture is moving. Tuturuuu is migrating the legacy Next.js
apps/web runtime (port 7803) to TanStack Start (apps/tanstack-web) plus a
dedicated Rust backend (apps/backend, port 7820). The emerging backend is
the Rust service, not a TypeScript “microservice.” See
TanStack Start And Rust Migration
for the canonical target. Treat the patterns below as portable design ideas, not
a permanent structural mandate.Core Concept
The hexagon represents your application’s core business logic. Ports are interfaces that define how the outside world can interact with your application, and Adapters are implementations that connect external technologies to these ports.How Tuturuuu Actually Structures Apps
Before the conceptual layers below, here is the real layout you will find in the codebase today. The Next.js apps are organized by concern, not by hexagonal layer:packages/internal-api and the
REST /api/v1 routes — not through tRPC, which in apps/web/src/trpc is
only a healthCheck stub today. Cross-app concerns live in packages/*
(packages/supabase, packages/payment, packages/trigger,
packages/internal-api, packages/ui, …), which is where most “swap the
adapter” flexibility actually lives in practice.
The sections that follow describe the idealized hexagonal layers as a
learning model. Read them for the concepts; do not expect to find these exact
directories in the repo.
The Idealized Layers (Conceptual)
1. Domain Layer (Core Business Logic)
Conceptual location:domain/ (illustrative — not a real directory in Tuturuuu)
Responsibilities:
- Pure business logic and rules
- Domain models (entities, value objects)
- Domain services (complex business operations)
- Domain events
- No dependencies on external libraries
- No knowledge of databases, APIs, or frameworks
- Fully unit-testable without infrastructure
2. Application Layer (Use Cases / Orchestration)
Conceptual location:application/ (illustrative — not a real directory in Tuturuuu)
Responsibilities:
- Orchestrate domain objects
- Implement use cases (business workflows)
- Transaction management
- Call infrastructure services via ports
3. Infrastructure Layer (Adapters)
Conceptual location:infrastructure/ (illustrative — not a real directory in Tuturuuu)
Responsibilities:
- Implement ports defined by application/domain
- Handle technology-specific details
- Database access, API calls, event publishing
4. Presentation Layer (API / UI)
Real location:apps/web/src/app/api/v1/ (REST handlers) or apps/web/src/app/
(routes/pages). This is the one layer that maps cleanly onto Tuturuuu’s actual
structure.
Responsibilities:
- HTTP request/response handling
- Input validation (DTOs)
- Authentication/authorization
- Call application use cases
createClient() from @tuturuuu/supabase/next/server
is async), checks auth via supabase.auth.getUser(), and then calls a helper.
The example below keeps the use-case indirection for teaching purposes:
Example (illustrative use-case wiring; Supabase client usage is real):
Key Benefits
1. Technology Independence
The core business logic has zero knowledge of the technologies used. We can swap Supabase for Prisma, Drizzle, or even MongoDB without touching business logic. Example migration:2. Testability
Domain logic can be tested without any infrastructure:3. Maintainability
Clear boundaries make it obvious where code belongs:- Business rules → Domain layer
- Workflows → Application layer
- Technology details → Infrastructure layer
- HTTP handling → Presentation layer
Ports (Interfaces)
Ports are contracts defined by the application/domain:Adapters (Implementations)
Adapters implement the ports:Dependency Injection
Wire adapters to use cases. Note thatcreateClient() is async, so the factory
must be async too:
Tuturuuu does not ship a formal DI container. Route handlers and
packages/internal-api helpers wire their dependencies directly. The factory
above shows the pattern conceptually.Directory Structure Example (Illustrative)
The tree below shows what a fully layered hexagonal app could look like. It is a teaching illustration — Tuturuuu does not use this layout. For the actual structure, see How Tuturuuu Actually Structures Apps above.C4 Component Diagram Explanations
The C4 model provides a structured way to visualize software architecture at different levels of abstraction. At the Component level, we focus on the major structural building blocks and their interactions within a system.Backend – Applying Ports and Adapters to Payments
The original version of this section described a Java/Kafka/Redis/Stripe payment
microservice. None of that exists in Tuturuuu. Payments are handled by
packages/payment, which currently has a single provider integration: Polar
(packages/payment/src/polar, with checkout/, client.ts, server.ts, and
Next.js helpers under next/). There is no Kafka, no Redis, no Stripe SDK, and
no Java. The dedicated backend is the Rust service in apps/backend
(route modules such as inventory.rs, aurora.rs, nova.rs,
onboarding_progress.rs), not a JVM microservice.packages/payment is
organized, even though the package does not enforce formal layers:
Inbound (Outside → Core):
Requests enter through HTTP route handlers (in apps/web /api/v1 today, and
increasingly in apps/backend Rust routes) and provider webhooks. These
translate transport payloads into typed inputs and call payment helpers — the
“inbound adapter” role.
Core (Business Rules):
Subscription tier logic, entitlement checks, and checkout orchestration are the
domain concern. Keeping these decisions independent of the provider is the goal:
the rest of the platform asks “what tier is this workspace?” without caring that
Polar answers it.
Outbound (Core → Outside):
The Polar provider in packages/payment/src/polar is the outbound adapter
to the payment processor. Persistence flows through the shared Supabase project
(packages/supabase), not a dedicated payment database. If Tuturuuu added a
second processor, it would implement the same client/checkout contract behind
the existing interface — which is exactly the substitutability hexagonal
architecture is designed to give you.
Why the pattern still helps here:
Even without formal domain//application//infrastructure/ directories,
isolating the provider behind packages/payment keeps the swap surface small.
That is the practical, real-world takeaway from this pattern in Tuturuuu — a
clean seam around an external dependency, not a strict layered file tree.
Frontend – Headless UI React Architecture (C4 Component Diagram)
The frontend separates rendering from state and behavioural logic to maintain scalability and reusability. This part of the description does broadly match Tuturuuu’s real conventions (Server Components by default, client hooks for interactivity, shared UI inpackages/ui).
Presentation Layer:
Each route/page acts as a top-level container that orchestrates data loading
and renders UI components. Visual components stay purely presentational, while
stateful or behavioural logic is delegated to hooks (apps/web/src/hooks,
typically TanStack Query for fetching and mutations). Per the platform rules,
data fetching is not done in useEffect.
Communication Layer:
Client-side data access flows through TanStack Query hooks, and app API
access is routed through the shared packages/internal-api helpers rather
than raw client-side fetch against app endpoints. Backend calls target the
REST /api/v1 routes today and, increasingly, the Rust apps/backend service.
Shared Component Library:
Reusable UI building blocks live in packages/ui (imported via
@tuturuuu/ui/*), with shared icons in packages/icons (@tuturuuu/icons),
supporting composability and reducing duplication across apps.
Layer Separation:
This architecture clearly separates Presentation → Logic → Communication layers, enabling maintainable state management, easier testability, and clean integration with backend APIs (Tuturuuu’s /api/v1 routes and the emerging Rust apps/backend). Since the diagram operates at C4 Component Level, it intentionally excludes internal React implementation details (such as local state, props, or context) and instead focuses on component roles, interactions, and the flow of data through the system.
Key Principles:
- Separation of Concerns: UI components are purely presentational; hooks handle state and behavior
- Reusability: Shared components and utilities reduce code duplication
- Testability: Clear boundaries enable isolated testing of presentation, logic, and communication layers
- Type Safety: Typed HTTP clients ensure contract adherence between frontend and backend
Related Documentation
- Architectural Decisions - Why we chose hexagonal architecture
- Encapsulation Patterns - Layer boundaries
- Data Fetching - Integration with React