Perfect for: Building maintainable, testable, and scalable SaaS applications with clear separation between UI, business logic, and data access.
Architecture Overview
The system follows a unidirectional data flow through three main layers:3-Layer Architecture:
๐ฅ๏ธ PRESENTATION LAYER
โโโ React Server Components (RSC)
โโโ Client Components ("use client")
โโโ Server Actions (mutations)
โโโ DAL (Data Access Layer with cache)
โฌ๏ธ
โ๏ธ SERVICE LAYER
โโโ Business Logic
โโโ Validation (Zod schemas)
โโโ Authorization (CASL RBAC)
โโโ Facades (interfaces)
โโโ Interceptors (logging, audit)
โฌ๏ธ
๐๏ธ PERSISTENCE LAYER
โโโ Drizzle ORM
โโโ Database Models
โโโ Repositories (DAO pattern)
โโโ Type Transformations
Core Architecture Principles
๐๏ธ
Layered Architecture
Strict 3-layer separation
โข Presentation: RSC, Client Components, Server Actions
โข Service: Business logic, validation, authorization
โข Persistence: Drizzle ORM, repositories, models
โข No layer bypassing: Each layer calls only the next
๐
Unidirectional Flow
Clear data movement
โข Reads: Component โ DAL โ Service โ Repository
โข Writes: Server Action โ Service โ Repository
โข Caching: DAL layer with
react-cacheโข Validation: Service layer with Zod schemas
๐
Security & Authorization
RBAC with CASL
โข Authorization: Every service operation checked
โข Validation: Input validation at service boundary
โข Type Safety: Domain types prevent data leaks
โข Audit Trail: Interceptors log all operations
โก
Performance
Optimized at every layer
โข Server-first: RSC by default
โข Caching: DAL layer with React cache
โข Database: Connection pooling with Drizzle
โข Client: Minimal client-side JavaScript
Example: Complete Data Flow
Here's how a typical user operation flows through all layers:Get User Profile:
// ๐ฅ๏ธ PRESENTATION (RSC Component)
import { getUserByIdDal } from '@/app/dal/user-dal'
export default async function UserProfile({ userId }: { userId: string }) {
const user = await getUserByIdDal(userId) // Cached read
return <div>{user.name}</div>
}
// ๐ DAL (Data Access Layer)
import { cache } from 'react'
import { getUserByIdService } from '@/services/facades/user-service-facade'
export const getUserByIdDal = cache(async (id: string) => {
const userModel = await getUserByIdService(id)
return transformToUserDTO(userModel) // Transform to domain type
})
// โ๏ธ SERVICE (Business Logic)
export const getUserByIdService = async (id: string) => {
const parsed = userUuidSchema.safeParse(id) // Validation
if (!parsed.success) throw new ValidationError()
const granted = await canReadUser(parsed.data) // Authorization
if (!granted) throw new AuthorizationError()
return await getUserByIdDao(parsed.data) // Call repository
}
// ๐๏ธ REPOSITORY (Data Access)
export async function getUserByIdDao(id: string): Promise<UserModel> {
return await db.query.users.findFirst({
where: eq(users.id, id)
})
}Key Benefits
โ
Maintainability
- โข Clear separation of concerns
- โข Easy to locate and fix issues
- โข Consistent patterns across features
- โข Self-documenting code structure
โ
Testability
- โข Each layer can be tested independently
- โข Easy mocking of dependencies
- โข Business logic isolated in services
- โข Database operations isolated in repositories
โ
Scalability
- โข Layers can scale independently
- โข Easy to add new features
- โข Caching at appropriate layers
- โข Performance optimizations per layer
Architecture Documentation
๐ฅ๏ธ
Presentation Layer
RSC, Client Components, Server Actions, DAL, and Formsโ๏ธ
Service Layer
Business logic, validation, authorization, facades, and interceptors๐๏ธ
Persistence Layer
Drizzle ORM, models, repositories, and type transformations๐ง
Helpers Architecture
Client/server separation and utility organization๐
Project Structure
File organization, naming conventions, and import rules๐งช
Development Workflow
Feature development process and testing strategiesArchitecture ready! Your application follows enterprise-grade patterns that ensure maintainability, testability, and scalability as your SaaS grows.