Skip to main content
Understanding how to structure code within services is critical for maintainability and testability. This document compares traditional N-Tier architecture with modern layer-based patterns, and explains how layering works within microservices.
Key Insight: Layering is an internal concern (how code is organized within a service), while microservices is an external concern (how services are distributed). These patterns are complementary, not mutually exclusive.
How to read this page. The N-Tier, Hexagonal, Clean, and Onion sections below are conceptual / educational material that explains layering theory with illustrative TypeScript. Tuturuuu’s apps do not ship a formal hexagonal directory layout (no domain/, application/, or infrastructure/ folders). Each app is a conventional Next.js App Router app (app/, components/, lib/, utils/, features/) that talks to a shared Supabase project, with the newer backend logic moving into the Rust service in apps/backend. Treat the layered code samples as patterns to learn from, not as a map of the current codebase. The “Tuturuuu’s Implementation” section near the end describes the actual layout.
Active migration. apps/web (Next.js, port 7803) is being replaced by apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). Backend layering increasingly lives in the Rust service. See TanStack Start And Rust Migration for the migration contract.
For a comprehensive comparison of all architectural patterns with detailed pros and cons, see Architectural Patterns Comparison.

Architectural Layering Patterns Compared

1. Traditional N-Tier Architecture

What Is N-Tier Architecture?

N-Tier (also called “N-Layer”) is a traditional architectural pattern that organizes code into horizontal layers, each responsible for a specific technical concern. The most common form is 3-tier architecture:

Characteristics of N-Tier

Dependencies Flow Downward:
  • Presentation → Business Logic → Data Access → Database
  • Each layer can only call the layer directly below it
  • Upper layers depend on concrete implementations in lower layers
Example (Traditional N-Tier):

Problems with Traditional N-Tier

  1. Tight Coupling to Infrastructure
    • Business logic depends directly on data access layer
    • Changing databases requires modifying business logic
    • Hard to test business rules without a database
  2. Leaky Abstractions
    • Database concerns leak into business logic (SQL, ORM entities)
    • Business logic becomes aware of persistence details
    • Domain models often have database annotations
  3. Circular Dependencies
    • Business layer creates data access objects
    • Data access returns domain models
    • Creates tight coupling between layers
  4. Technology Lock-In
    • Business logic married to specific ORM or database
    • Difficult to swap technologies
    • Framework dependencies throughout codebase
Example of the Problem:

2. Modern Layer-Based Architectures

Layer-based architectures (Hexagonal, Clean, Onion) solve N-Tier’s problems by inverting dependencies and organizing around business domains rather than technical layers.

Hexagonal Architecture (Ports & Adapters)

Key Principles:
  1. Dependency Inversion: Domain defines interfaces (ports), infrastructure implements them (adapters)
  2. Domain Core is Pure: No infrastructure dependencies, no framework code
  3. Testability: Domain can be tested in isolation with test doubles
  4. Technology Agnostic: Infrastructure can be swapped without changing business logic
Example (Hexagonal Architecture):

Benefits of Hexagonal Architecture

Domain Logic is Pure and Testable
Technology Can Be Swapped Easily
Fast, Reliable Tests

3. Clean Architecture

Clean Architecture (by Robert C. Martin) is similar to Hexagonal but emphasizes concentric circles of dependencies. Key Rule: Dependencies can only point inward. Inner circles know nothing about outer circles. Example Structure (illustrative — these paths are not present in the repo):

4. Onion Architecture

Onion Architecture is similar to Clean but visualizes layers as concentric circles with explicit layer names. All three patterns (Hexagonal, Clean, Onion) share the same core principle: Dependency Inversion to keep business logic pure and independent.

Layering Within Microservices

Microservices ≠ No Layering

Common Misconception: “Microservices replace layering.” Reality: Microservices is an external architectural pattern (how services are distributed across the network). Layering is an internal pattern (how code is organized within each service).

Illustrative: Hexagonal Architecture Within a Service

The directory tree below is an illustrative target showing how a service could be organized with strict hexagonal layers. It is not the current Tuturuuu layout — apps/web/src and apps/finance/src do not contain domain/, application/, or infrastructure/ folders. See Tuturuuu’s Implementation for the real structure.
Why a team might adopt this approach:
  1. Service Isolation: Each deployable unit stays independently shippable
  2. Internal Quality: Each service maintains clean internal architecture
  3. Technology Freedom: Each service can use different infrastructure
  4. Testability: Domain logic in each service is pure and testable
  5. Maintainability: Clear structure within each service
In practice, Tuturuuu keeps most code in conventional Next.js folders and pushes heavier backend logic into the Rust service (apps/backend) rather than introducing a formal hexagonal folder hierarchy inside each frontend app.

Layer-Based Architecture Inside Microservices (Deep Dive)

While microservices define how services communicate externally, the internal structure of each service is equally important. This section provides a comprehensive guide to implementing layer-based architecture within microservices.

Why Layering Matters in Microservices

Many teams make the mistake of thinking: “We have microservices, so we don’t need internal structure.” This leads to:
  • Business logic mixed with HTTP handling
  • Database queries scattered throughout the codebase
  • Difficult-to-test services
  • Technology coupling within services
The reality: Each microservice should have clean internal architecture to maintain quality as services grow.

Complete Layer-by-Layer Breakdown

Layer 1: Domain Core (Innermost)

Purpose: Contains pure business logic with zero external dependencies. Contents:
  • Entities (business objects with identity)
  • Value Objects (immutable business concepts)
  • Domain Services (complex business operations)
  • Business Rules (validation, constraints)
  • Domain Events (things that happened)
Example - Complete Domain Layer (illustrative path; the repo does not contain a src/domain/ folder):
Testing Domain Layer:

Layer 2: Application Layer (Use Cases)

Purpose: Orchestrates domain entities and infrastructure to fulfill application workflows. Contents:
  • Use Cases (application workflows)
  • Commands (input data structures)
  • Application Services (workflow orchestration)
  • DTOs (data transfer objects)
Example - Complete Application Layer:
Testing Application Layer:

Layer 3: Infrastructure Layer (Adapters)

Purpose: Implements infrastructure concerns and connects to external systems. Contents:
  • Repository Implementations (database access)
  • Event Publishers (message brokers)
  • External Service Clients (HTTP, gRPC)
  • Caching Implementations
  • File Storage Implementations
Example - Complete Infrastructure Layer:
For reference, the matching Trigger.dev v4 task definition looks like the real tasks in packages/trigger/src:

Layer 4: Presentation Layer (API/UI)

Purpose: Handles HTTP requests, validates input, and returns responses. Contents:
  • API Route Handlers
  • Request/Response DTOs
  • Input Validation
  • Authentication/Authorization
  • HTTP Status Codes
Example - Complete Presentation Layer:

Layer Communication Patterns

Testing Strategy Per Layer

Comparison: N-Tier vs Hexagonal in Microservices

N-Tier within Microservice:
  • ❌ Business logic coupled to database
  • ❌ Hard to test without infrastructure
  • ❌ Technology lock-in within service
  • ✅ Simpler for trivial services
Hexagonal within Microservice:
  • ✅ Business logic is pure and testable
  • ✅ Technology can be swapped per service
  • ✅ High test coverage achievable
  • ❌ More code and structure needed

Deployment Considerations

Each app deploys independently. Tuturuuu does not expose bun deploy:web / bun deploy:finance / bun deploy:calendar scripts — deployment is driven by CI/CD (GitHub Actions) and Docker images, and each app’s package.json ships conventional dev, build, start, and test scripts (for example apps/web builds with next build --turbopack). Verify the current scripts in each app’s package.json before quoting commands.

Key Takeaways

  1. Microservices ≠ No Internal Structure: Each service needs clean internal architecture
  2. Hexagonal Within Services: Provides testability and maintainability per service
  3. Layer Discipline: Strict layer boundaries prevent architectural decay
  4. Independent Evolution: Each service can evolve its internals independently
  5. Consistent Patterns: Same layering approach across services aids understanding

Comparison Matrix


When to Use Each Pattern

Use N-Tier When:

  • ✅ Building simple CRUD applications
  • ✅ Team is unfamiliar with DDD/Hexagonal concepts
  • ✅ Rapid prototyping with acceptable technical debt
  • ✅ Application will remain small (<10k LOC)

Use Hexagonal/Clean/Onion When:

  • ✅ Complex business logic requires protection
  • ✅ Long-term maintainability is critical
  • ✅ Need to swap infrastructure components
  • ✅ High test coverage is required
  • ✅ Domain experts are involved in development

Use Microservices When:

  • ✅ Multiple teams working on different domains
  • ✅ Need independent deployment and scaling
  • ✅ Different parts of system have different technology needs
  • ✅ Can handle distributed system complexity
  • ✅ Have DevOps maturity for service orchestration

Tuturuuu’s Pragmatic Choice

Tuturuuu favors pragmatism over textbook purity:
  • Multiple deployable apps (apps/web, apps/finance, apps/calendar, the Rust apps/backend, …) for organizational agility and independent deployment.
  • Conventional Next.js App Router structure inside each frontend app (app/, components/, lib/, utils/, features/) rather than a formal hexagonal domain/application/infrastructure hierarchy. Shared logic lives in packages/*; cross-app data access goes through packages/internal-api helpers and REST /api/v1 routes.
  • Heavier backend logic moving into the Rust service (apps/backend), where layering and typed domain modules increasingly live as part of the TanStack + Rust migration.
The hexagonal samples above remain useful for reasoning about boundaries and testability, even though the repository does not enforce that exact folder shape.

Evolution Path

Many systems evolve through these patterns: Where Tuturuuu sits today. Rather than formally adopting “Step 3” (microservices with strict hexagonal layering), Tuturuuu runs several conventional Next.js App Router apps backed by a shared Supabase project, with shared logic in packages/* and an emerging Rust backend (apps/backend). The practical trajectory is toward the TanStack Start frontend (apps/tanstack-web) plus the Rust backend rather than toward per-app hexagonal folder hierarchies — see the TanStack + Rust migration. Treat the steps above as a conceptual maturity model, not a literal description of the repository.

Anti-Patterns to Avoid

❌ Hexagonal Architecture Without Discipline

❌ Distributed Monolith

❌ No Layering in Microservices


Tuturuuu’s Implementation

Actual Project Structure

This is the real repository layout (verified against the codebase). Apps are conventional Next.js App Router apps — there are no domain/, application/, or infrastructure/ folders.
Where backend layering lives. Newer/heavier backend logic is implemented in the Rust service (apps/backend/src, e.g. aurora.rs, inventory.rs, onboarding_progress.rs) rather than as a layered TypeScript hierarchy inside each frontend app. Frontends reach shared app data through packages/internal-api and REST /api/v1 routes. (A repo guard, check-tanstack-api-access, forbids apps/tanstack-web/src from calling /trpc — the tRPC surface in apps/web/src/trpc is a stub, not the product data path.)

How the Real Flow Maps to These Layers

You can still reason about a real Tuturuuu request in layered terms, even though the folders are flat:
  • Presentation — an App Router route handler in apps/web/src/app/api/... (or a TanStack Start route in apps/tanstack-web).
  • Application / orchestration — helpers in lib/, features/, packages/internal-api, or background task() runs in packages/trigger.
  • Domain rules — validation and business logic colocated with the feature (or enforced in the Rust backend).
  • Infrastructure — the shared Supabase client from @tuturuuu/supabase/next/server and the Rust backend’s data access.
The layered patterns earlier in this page are a lens for keeping responsibilities clear; the snippet above shows what that looks like in the conventional structure Tuturuuu actually ships.