Skip to main content

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:
Product data flows through shared helpers in 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
Characteristics:
  • No dependencies on external libraries
  • No knowledge of databases, APIs, or frameworks
  • Fully unit-testable without infrastructure
Example (illustrative — this file does not exist in the repo):

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
Example (illustrative):

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
Example (illustrative):

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
In real Tuturuuu code, a route handler does its own validation, gets an awaited Supabase client (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:
Application layer with mocks:

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 that createClient() 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.
The ports-and-adapters idea still maps cleanly onto how 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 in packages/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:
  1. Separation of Concerns: UI components are purely presentational; hooks handle state and behavior
  2. Reusability: Shared components and utilities reduce code duplication
  3. Testability: Clear boundaries enable isolated testing of presentation, logic, and communication layers
  4. Type Safety: Typed HTTP clients ensure contract adherence between frontend and backend