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.
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
Problems with Traditional N-Tier
-
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
-
Leaky Abstractions
- Database concerns leak into business logic (SQL, ORM entities)
- Business logic becomes aware of persistence details
- Domain models often have database annotations
-
Circular Dependencies
- Business layer creates data access objects
- Data access returns domain models
- Creates tight coupling between layers
-
Technology Lock-In
- Business logic married to specific ORM or database
- Difficult to swap technologies
- Framework dependencies throughout codebase
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:- Dependency Inversion: Domain defines interfaces (ports), infrastructure implements them (adapters)
- Domain Core is Pure: No infrastructure dependencies, no framework code
- Testability: Domain can be tested in isolation with test doubles
- Technology Agnostic: Infrastructure can be swapped without changing business logic
Benefits of Hexagonal Architecture
✅ Domain Logic is Pure and Testable3. 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
- Service Isolation: Each deployable unit stays independently shippable
- Internal Quality: Each service maintains clean internal architecture
- Technology Freedom: Each service can use different infrastructure
- Testability: Domain logic in each service is pure and testable
- Maintainability: Clear structure within each 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
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)
src/domain/ folder):
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)
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
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
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
- ✅ 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 exposebun 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
- Microservices ≠ No Internal Structure: Each service needs clean internal architecture
- Hexagonal Within Services: Provides testability and maintainability per service
- Layer Discipline: Strict layer boundaries prevent architectural decay
- Independent Evolution: Each service can evolve its internals independently
- 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 Rustapps/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 hexagonaldomain/application/infrastructurehierarchy. Shared logic lives inpackages/*; cross-app data access goes throughpackages/internal-apihelpers and REST/api/v1routes. - 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.
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 inpackages/* 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 nodomain/, 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 inapps/tanstack-web). - Application / orchestration — helpers in
lib/,features/,packages/internal-api, or backgroundtask()runs inpackages/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/serverand the Rust backend’s data access.
Related Documentation
- Hexagonal Architecture - Detailed ports and adapters implementation
- Microservices Patterns - Service boundaries and communication
- Architectural Decisions - Why we chose this architecture
- Quality Attributes - Maintainability and testability benefits
- Encapsulation Patterns - Preventing unwanted coupling