Skip to content

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.ts

Boundaries 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.

src/presentation/controllers/user-controller.ts
// βœ… GOOD - Presentation calling Application
import { UserService } from '../../application/services/user-service'
export class UserController {
constructor(private userService: UserService) {}
async createUser(req: Request) {
return await this.userService.create(req.body)
}
}
src/application/services/user-service.ts
// ❌ BAD - Application calling Presentation
import { 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.

src/application/services/user-service.ts
// ❌ BAD
import { UserController } from '../../presentation/user-controller'
// βœ… GOOD
// src/application/services/user-service.ts
import { 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.

src/domain/entities/user.ts
// ❌ BAD
import { UserService } from '../../application/services/user-service'
// βœ… GOOD
// src/domain/entities/user.ts
export 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.

src/infrastructure/repositories/user-repository.ts
// ❌ BAD
import { UserService } from '../../application/services/user-service'
// βœ… GOOD
// src/infrastructure/repositories/user-repository.ts
import { 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

src/presentation/controllers/user-controller.ts
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

src/application/services/user-service.ts
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

src/domain/entities/user.ts
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
}
}
src/domain/services/user-domain-service.ts
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

src/infrastructure/repositories/user-repository.ts
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

src/infrastructure/repositories/user-repository.ts
// ❌ BAD
import { UserService } from '../../application/services/user-service'
export class UserRepository {
constructor(private userService: UserService) {} // Violates dependency flow!
}

Fix: Invert the dependency

src/application/services/user-service.ts
// βœ… GOOD
import { UserRepository } from '../../infrastructure/repositories/user-repository'
export class UserService {
constructor(private userRepo: UserRepository) {} // Correct direction
}

Violation: Domain importing application

src/domain/entities/user.ts
// ❌ BAD
import { UserService } from '../../application/services/user-service'
export class User {
async save() {
await new UserService().createUser(this) // Wrong!
}
}

Fix: Keep domain pure, let application orchestrate

src/domain/entities/user.ts
// βœ… GOOD
export class User {
validateEmail(): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)
}
}
// src/application/services/user-service.ts
export 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

AspectLayeredHexagonal
Domain isolationModerateStrict
Learning curveLowMedium
AbstractionLessMore
Best forTraditional appsDDD, complex domains

vs. Clean Architecture

AspectLayeredClean
Dependency directionTop-downInward
Framework couplingPossibleAvoided
ComplexityLowerHigher
Best forEstablished teamsEnterprise apps

Next Steps