Domain-Driven Design (DDD) Architecture

Learn how to structure complex Nuxt applications using Domain-Driven Design principles for maintainable, scalable business applications.

Why DDD for Nuxt Applications

  • ✅ Clear separation of business logic and technical concerns
  • ✅ Better scalability for large teams and applications
  • ✅ Improved code maintainability and testability
  • ✅ Align code structure with business domains

Understanding Bounded Contexts

A bounded context defines clear boundaries around a specific domain model. In Nuxt, each bounded context can be a separate feature module.

Project Structure

nuxt-app/
├── modules/
│   ├── users/           # User Management Domain
│   │   ├── composables/
│   │   ├── components/
│   │   ├── server/
│   │   ├── types/
│   │   └── module.ts
│   ├── orders/          # Order Management Domain
│   │   ├── composables/
│   │   ├── components/
│   │   ├── server/
│   │   ├── types/
│   │   └── module.ts
│   └── billing/         # Billing Domain
│       ├── composables/
│       ├── components/
│       ├── server/
│       ├── types/
│       └── module.ts
└── nuxt.config.ts

Creating a Feature Module

User Domain Module

// modules/users/module.ts
import { defineNuxtModule, createResolver } from '@nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'users',
    configKey: 'users'
  },
  setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)
    
    // Add auto-imports for composables
    nuxt.hook('imports:dirs', (dirs) => {
      dirs.push(resolver.resolve('./composables'))
    })
    
    // Add components
    nuxt.hook('components:dirs', (dirs) => {
      dirs.push({
        path: resolver.resolve('./components'),
        prefix: 'User'
      })
    })
    
    // Add server routes
    nuxt.hook('nitro:config', (nitroConfig) => {
      nitroConfig.serverHandlers = nitroConfig.serverHandlers || []
      nitroConfig.serverHandlers.push({
        route: '/api/users/**',
        handler: resolver.resolve('./server/api/users/index.ts')
      })
    })
  }
})

Domain Types

// modules/users/types/index.ts
export interface User {
  id: string
  email: string
  profile: UserProfile
  role: UserRole
}

export interface UserProfile {
  firstName: string
  lastName: string
  avatar?: string
}

export enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest'
}

export interface CreateUserCommand {
  email: string
  password: string
  profile: UserProfile
}

export interface UpdateUserCommand {
  id: string
  profile: Partial<UserProfile>
}

Domain Service

// modules/users/composables/useUserService.ts
import type { User, CreateUserCommand, UpdateUserCommand } from '../types'

export function useUserService() {
  const users = ref<User[]>([])
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  async function createUser(command: CreateUserCommand): Promise<User> {
    loading.value = true
    error.value = null
    
    try {
      const user = await $fetch<User>('/api/users', {
        method: 'POST',
        body: command
      })
      
      users.value.push(user)
      return user
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }
  
  async function updateUser(command: UpdateUserCommand): Promise<User> {
    loading.value = true
    error.value = null
    
    try {
      const user = await $fetch<User>(`/api/users/${command.id}`, {
        method: 'PATCH',
        body: command
      })
      
      const index = users.value.findIndex(u => u.id === command.id)
      if (index !== -1) {
        users.value[index] = user
      }
      
      return user
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }
  
  async function fetchUsers(): Promise<User[]> {
    loading.value = true
    error.value = null
    
    try {
      users.value = await $fetch<User[]>('/api/users')
      return users.value
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }
  
  return {
    users: readonly(users),
    loading: readonly(loading),
    error: readonly(error),
    createUser,
    updateUser,
    fetchUsers
  }
}

Server-side Repository Pattern

// modules/users/server/repositories/UserRepository.ts
import type { User, CreateUserCommand } from '../../types'

export class UserRepository {
  private db: any // Your database client
  
  constructor(db: any) {
    this.db = db
  }
  
  async findById(id: string): Promise<User | null> {
    return await this.db.users.findUnique({
      where: { id }
    })
  }
  
  async findByEmail(email: string): Promise<User | null> {
    return await this.db.users.findUnique({
      where: { email }
    })
  }
  
  async create(command: CreateUserCommand): Promise<User> {
    return await this.db.users.create({
      data: {
        email: command.email,
        password: await hashPassword(command.password),
        profile: {
          create: command.profile
        }
      },
      include: {
        profile: true
      }
    })
  }
  
  async update(id: string, data: Partial<User>): Promise<User> {
    return await this.db.users.update({
      where: { id },
      data,
      include: {
        profile: true
      }
    })
  }
  
  async delete(id: string): Promise<void> {
    await this.db.users.delete({
      where: { id }
    })
  }
}

function hashPassword(password: string): Promise<string> {
  // Implementation
  return Promise.resolve(password)
}

Cross-Domain Communication

Domain Events

// modules/shared/events/DomainEvent.ts
export interface DomainEvent {
  type: string
  timestamp: Date
  payload: any
}

// modules/users/events/UserCreatedEvent.ts
import type { DomainEvent } from '~/modules/shared/events/DomainEvent'
import type { User } from '../types'

export interface UserCreatedEvent extends DomainEvent {
  type: 'user.created'
  payload: {
    user: User
  }
}

Event Bus

// modules/shared/composables/useEventBus.ts
import mitt from 'mitt'
import type { DomainEvent } from '../events/DomainEvent'

const emitter = mitt<Record<string, DomainEvent>>()

export function useEventBus() {
  function publish(event: DomainEvent) {
    emitter.emit(event.type, event)
  }
  
  function subscribe<T extends DomainEvent>(
    eventType: string,
    handler: (event: T) => void
  ) {
    emitter.on(eventType, handler as any)
    
    return () => emitter.off(eventType, handler as any)
  }
  
  return {
    publish,
    subscribe
  }
}

Using Events

// modules/billing/composables/useBillingService.ts
export function useBillingService() {
  const { subscribe } = useEventBus()
  
  // Listen to user creation to create billing account
  onMounted(() => {
    const unsubscribe = subscribe('user.created', async (event) => {
      await createBillingAccount(event.payload.user.id)
    })
    
    onUnmounted(unsubscribe)
  })
  
  async function createBillingAccount(userId: string) {
    await $fetch('/api/billing/accounts', {
      method: 'POST',
      body: { userId }
    })
  }
  
  return {
    createBillingAccount
  }
}

Register Modules in Nuxt Config

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '~/modules/users/module',
    '~/modules/orders/module',
    '~/modules/billing/module'
  ]
})

Best Practices

✅ Do's

  1. ✅ Keep domains independent and loosely coupled
  2. ✅ Use domain events for cross-domain communication
  3. ✅ Define clear boundaries and interfaces
  4. ✅ Use TypeScript interfaces for domain contracts
  5. ✅ Apply repository pattern for data access
  6. ✅ Use command objects for write operations
  7. ✅ Organize by feature, not by technical layer

❌ Don'ts

  1. ❌ Don't share database models directly between domains
  2. ❌ Don't create circular dependencies between modules
  3. ❌ Don't leak domain logic to presentation layer
  4. ❌ Don't bypass domain services with direct API calls
  5. ❌ Don't mix multiple domains in a single component

Advanced Patterns

Aggregate Root

// modules/orders/domain/Order.ts
export class Order {
  constructor(
    public readonly id: string,
    public customerId: string,
    private _items: OrderItem[],
    private _status: OrderStatus
  ) {}
  
  get items(): readonly OrderItem[] {
    return this._items
  }
  
  get status(): OrderStatus {
    return this._status
  }
  
  get total(): number {
    return this._items.reduce((sum, item) => sum + item.total, 0)
  }
  
  addItem(item: OrderItem): void {
    if (this._status !== OrderStatus.Draft) {
      throw new Error('Cannot add items to non-draft order')
    }
    this._items.push(item)
  }
  
  submit(): void {
    if (this._items.length === 0) {
      throw new Error('Cannot submit empty order')
    }
    this._status = OrderStatus.Submitted
  }
  
  cancel(): void {
    if (this._status === OrderStatus.Shipped) {
      throw new Error('Cannot cancel shipped order')
    }
    this._status = OrderStatus.Cancelled
  }
}

Value Objects

// modules/shared/domain/Email.ts
export class Email {
  private constructor(private readonly value: string) {}
  
  static create(email: string): Email {
    if (!this.isValid(email)) {
      throw new Error('Invalid email address')
    }
    return new Email(email.toLowerCase())
  }
  
  static isValid(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }
  
  toString(): string {
    return this.value
  }
  
  equals(other: Email): boolean {
    return this.value === other.value
  }
}

DDD helps structure complex Nuxt applications by aligning code with business domains, making them more maintainable and scalable.

WebSockets and Real-time Communication
Enable real-time features in your Nuxt application with WebSockets for live updates, notifications, and collaborative features.
Advanced Data Tables
Build enterprise-grade data tables with filtering, sorting, pagination, inline editing, bulk actions, and export capabilities.
Files
Editor
Initializing WebContainer
Mounting files
Installing dependencies
Starting Nuxt server
Waiting for Nuxt to ready
Terminal