Layered Architecture Preset
The Layered Architecture preset enforces the traditional N-tier pattern where dependencies flow from top to bottom through well-defined layers.
What is Layered Architecture?
Layered Architecture (also called N-tier architecture) is a classic pattern that:
- Organizes code into layers based on technical concerns
- Enforces top-to-bottom dependencies (no circular dependencies)
- Separates UI, logic, and data into distinct layers
- Allows each layer to depend on layers below it
The core principle: Dependencies flow downward only.
When to Use This Preset
Use the Layered preset when you have:
- Traditional application structure familiar to your team
- Straightforward business logic without complex domain models
- Established patterns already in place
- Team experience with N-tier architecture
- Need for simplicity over strict domain isolation
Architecture Diagram
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor':'#b3d9ff',
'primaryBorderColor':'#1976d2',
'secondaryColor':'#b3ffcc',
'secondaryBorderColor':'#2e7d32',
'tertiaryColor':'#ffffb3',
'tertiaryBorderColor':'#f57c00'
}}}%%
graph TB
subgraph Presentation["π Presentation Layer
Layer 0 - UI & Controllers"]
Controllers["Controllers"]
Views["Views"]
CLI["CLI"]
end
subgraph Application["π Application Layer
Layer 1 - Services & Use Cases"]
Services["Application Services"]
UseCases["Use Cases"]
end
subgraph Domain["π Domain Layer
Layer 2 - Business Logic"]
Entities["Entities"]
DomainServices["Domain Services"]
end
subgraph Infrastructure["π Infrastructure Layer
Layer 3 - Data & External Systems"]
Repositories["Repositories"]
Database["Database Access"]
External["External APIs"]
end
%% Allowed dependencies (green arrows)
Controllers --> Services
Views --> Services
CLI --> Services
Controllers --> Domain
Views --> Domain
CLI --> Domain
Controllers --> Repositories
Views --> Repositories
Services --> Domain
Services --> Repositories
Domain --> Repositories
Repositories --> Domain
style Presentation fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Application fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px
style Domain fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
style Infrastructure fill:#fff3e0,stroke:#f57c00,stroke-width:2px
linkStyle default stroke:#22c55e,stroke-width:2px
Directory Structure
src/βββ presentation/ # Layer 0 - Topβ βββ controllers/β β βββ user-controller.tsβ βββ views/β β βββ user-view.tsxβ βββ cli/β βββ commands.tsβββ application/ # Layer 1β βββ services/β β βββ user-service.tsβ βββ use-cases/β βββ create-user.tsβββ domain/ # Layer 2β βββ entities/β β βββ user.tsβ βββ services/β βββ user-domain-service.tsβββ infrastructure/ # Layer 3 - Bottom βββ repositories/ β βββ user-repository.ts βββ database/ β βββ connection.ts βββ external/ βββ email-client.tsBoundaries Defined
The preset defines four layers:
1. Presentation
- Pattern:
src/presentation/** - Tags:
presentation,ui - Purpose: User interface, controllers, views, CLI
- Layer: 0 (top)
- Can depend on: Application, Domain, Infrastructure
2. Application
- Pattern:
src/application/** - Tags:
application,services - Purpose: Use cases, application services, orchestration
- Layer: 1
- Can depend on: Domain, Infrastructure
3. Domain
- Pattern:
src/domain/** - Tags:
domain,core - Purpose: Business logic, entities, domain services
- Layer: 2
- Can depend on: Infrastructure (for repository interfaces)
4. Infrastructure
- Pattern:
src/infrastructure/** - Tags:
infrastructure,data - Purpose: Data access, external systems, persistence
- Layer: 3 (bottom)
- Can depend on: Domain (for entities)
Key Rules Enforced
Top-to-Bottom Dependencies Only
Each layer can only depend on layers below it. No upward dependencies allowed.
// β
GOOD - Presentation calling Applicationimport { UserService } from '../../application/services/user-service'
export class UserController { constructor(private userService: UserService) {}
async createUser(req: Request) { return await this.userService.create(req.body) }}// β BAD - Application calling Presentationimport { UserController } from '../../presentation/controllers/user-controller'// This violates the dependency flow!Application Layer Cannot Depend on Presentation
Application layer provides services, it doesnβt know about UI.
// β BADimport { UserController } from '../../presentation/user-controller'
// β
GOOD// src/application/services/user-service.tsimport { User } from '../../domain/entities/user'import { UserRepository } from '../../infrastructure/repositories/user-repository'
export class UserService { constructor(private userRepo: UserRepository) {}
async create(data: CreateUserData): Promise<User> { const user = new User(data) return await this.userRepo.save(user) }}Domain Layer Cannot Depend on Application or Presentation
Domain contains core business rules, independent of use cases or UI.
// β BADimport { UserService } from '../../application/services/user-service'
// β
GOOD// src/domain/entities/user.tsexport class User { constructor( public readonly id: string, public email: string, public name: string ) {}
validateEmail(): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email) }}Infrastructure Layer Cannot Depend on Application or Presentation
Infrastructure is the bottom layer, providing data access.
// β BADimport { UserService } from '../../application/services/user-service'
// β
GOOD// src/infrastructure/repositories/user-repository.tsimport { User } from '../../domain/entities/user'import { Database } from '../database/connection'
export class UserRepository { constructor(private db: Database) {}
async save(user: User): Promise<User> { const result = await this.db.query( 'INSERT INTO users (id, email, name) VALUES ($1, $2, $3)', [user.id, user.email, user.name] ) return user }}Example Configuration
{ "preset": "@stricture/layered"}No additional configuration needed for standard layered structure.
Real Code Example
Presentation Layer
import { Request, Response } from 'express'import { UserService } from '../../application/services/user-service'
export class UserController { constructor(private userService: UserService) {}
async create(req: Request, res: Response) { try { const user = await this.userService.createUser(req.body) res.status(201).json(user) } catch (error) { res.status(400).json({ error: error.message }) } }
async getById(req: Request, res: Response) { const user = await this.userService.getUserById(req.params.id) if (!user) { return res.status(404).json({ error: 'User not found' }) } res.json(user) }}Application Layer
import { User } from '../../domain/entities/user'import { UserRepository } from '../../infrastructure/repositories/user-repository'import { EmailService } from '../../infrastructure/external/email-service'
export interface CreateUserData { email: string name: string}
export class UserService { constructor( private userRepo: UserRepository, private emailService: EmailService ) {}
async createUser(data: CreateUserData): Promise<User> { // Validate if (!data.email || !data.name) { throw new Error('Email and name are required') }
// Check if exists const existing = await this.userRepo.findByEmail(data.email) if (existing) { throw new Error('User already exists') }
// Create entity const user = new User( crypto.randomUUID(), data.email, data.name )
// Validate domain rules if (!user.validateEmail()) { throw new Error('Invalid email format') }
// Save const saved = await this.userRepo.save(user)
// Send welcome email await this.emailService.sendWelcome(user.email)
return saved }
async getUserById(id: string): Promise<User | null> { return await this.userRepo.findById(id) }}Domain Layer
export class User { constructor( public readonly id: string, public email: string, public name: string ) {}
validateEmail(): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email) }
changeName(newName: string): void { if (!newName || newName.trim().length === 0) { throw new Error('Name cannot be empty') } this.name = newName }}import { User } from '../entities/user'
export class UserDomainService { canUserBeDeleted(user: User, orderCount: number): boolean { // Business rule: Users with active orders cannot be deleted return orderCount === 0 }
generateUsername(user: User): string { return user.email.split('@')[0].toLowerCase() }}Infrastructure Layer
import { User } from '../../domain/entities/user'import { Pool } from 'pg'
export class UserRepository { constructor(private pool: Pool) {}
async save(user: User): Promise<User> { await this.pool.query( `INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET email = $2, name = $3`, [user.id, user.email, user.name] ) return user }
async findById(id: string): Promise<User | null> { const result = await this.pool.query( 'SELECT * FROM users WHERE id = $1', [id] )
if (result.rows.length === 0) { return null }
const row = result.rows[0] return new User(row.id, row.email, row.name) }
async findByEmail(email: string): Promise<User | null> { const result = await this.pool.query( 'SELECT * FROM users WHERE email = $1', [email] )
if (result.rows.length === 0) { return null }
const row = result.rows[0] return new User(row.id, row.email, row.name) }}Common Violations and Fixes
Violation: Lower layer importing higher layer
// β BADimport { UserService } from '../../application/services/user-service'
export class UserRepository { constructor(private userService: UserService) {} // Violates dependency flow!}Fix: Invert the dependency
// β
GOODimport { UserRepository } from '../../infrastructure/repositories/user-repository'
export class UserService { constructor(private userRepo: UserRepository) {} // Correct direction}Violation: Domain importing application
// β BADimport { UserService } from '../../application/services/user-service'
export class User { async save() { await new UserService().createUser(this) // Wrong! }}Fix: Keep domain pure, let application orchestrate
// β
GOODexport class User { validateEmail(): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email) }}
// src/application/services/user-service.tsexport class UserService { async createUser(data: CreateUserData): Promise<User> { const user = new User(id, data.email, data.name) user.validateEmail() // Application calls domain return await this.userRepo.save(user) }}Benefits
- Simplicity: Easy to understand and explain
- Familiarity: Most developers know this pattern
- Clear separation: Each layer has a clear purpose
- Tooling support: IDEs and frameworks support this structure well
Trade-offs
- Less domain isolation: Domain can depend on infrastructure
- Potential coupling: Easier to create tight coupling between layers
- Database-centric: Often becomes database-driven instead of domain-driven
Comparison with Other Patterns
vs. Hexagonal Architecture
| Aspect | Layered | Hexagonal |
|---|---|---|
| Domain isolation | Moderate | Strict |
| Learning curve | Low | Medium |
| Abstraction | Less | More |
| Best for | Traditional apps | DDD, complex domains |
vs. Clean Architecture
| Aspect | Layered | Clean |
|---|---|---|
| Dependency direction | Top-down | Inward |
| Framework coupling | Possible | Avoided |
| Complexity | Lower | Higher |
| Best for | Established teams | Enterprise apps |
Related Patterns
- Hexagonal Architecture - Stricter domain isolation
- Clean Architecture - Concentric circles with inward dependencies
Next Steps
- Check out the layered example
- Learn about customizing presets
- Compare with other architectural patterns