Skip to main content

DDD + Clean Architecture

MStore Dashboard mengimplementasikan Domain-Driven Design (DDD) dengan Clean Architecture untuk memastikan codebase yang maintainable, testable, dan scalable.

Architecture Overview

Directory Structure

mstore_dashboard/

├─ app/                          # Presentation Layer
│   ├─ layouts/                  # Layout components
│   │   ├─ default.vue          # Main layout with navigation
│   │   └─ auth.vue             # Auth pages layout
│   ├─ pages/                    # Route pages
│   │   ├─ index.vue            # Home page
│   │   ├─ login.vue            # Login page
│   │   └─ inventory/           # Inventory pages
│   ├─ middleware/               # Route middleware
│   │   └─ auth.global.ts       # Auth guard
│   └─ error.vue                 # Error page

├─ features/                     # Feature/Domain Layer
│   ├─ 01_core/                 # Core domain
│   │   ├─ api/                 # API calls
│   │   ├─ components/          # Domain components
│   │   ├─ composables/         # Domain logic
│   │   ├─ store/               # Pinia stores
│   │   ├─ types/               # TypeScript types
│   │   └─ index.ts             # Barrel export
│   │
│   ├─ 03_inventory/            # Inventory domain
│   │   ├─ api/
│   │   │   ├─ getList.ts
│   │   │   ├─ create.ts
│   │   │   └─ update.ts
│   │   ├─ components/
│   │   │   └─ InventoryTable.vue
│   │   ├─ composables/
│   │   │   └─ useInventoryList.ts
│   │   ├─ store/
│   │   │   └─ inventory.store.ts
│   │   ├─ types/
│   │   │   └─ inventory.types.ts
│   │   └─ index.ts
│   │
│   └─ ... (other domains)

├─ components/                   # Global Components
│   ├─ AppSpreadsheet.vue       # Shared spreadsheet
│   └─ NotificationDropdown.vue # Notifications

├─ composables/                  # Global Composables
│   ├─ useAuth.ts               # Auth utilities
│   └─ useDebounce.ts           # Debounce utility

├─ server/                       # Backend Layer (Nitro)
│   ├─ api/                     # API endpoints
│   └─ middleware/              # Server middleware

├─ utils/                        # Pure Functions
│   ├─ format.ts                # Formatting utilities
│   └─ validator.ts             # Validation functions

└─ types/                        # Global Types
    └─ index.ts                 # Shared type definitions

Architecture Layers

1. Presentation Layer

Bertanggung jawab untuk:
  • Rendering UI
  • Route handling
  • User interactions
  • Layout management
<!-- app/pages/inventory/index.vue -->
<script setup lang="ts">
import { useInventoryList } from '~/features/03_inventory'

const { items, loading, fetchItems } = useInventoryList()

onMounted(() => fetchItems())
</script>

<template>
  <div>
    <h1>Inventory</h1>
    <InventoryTable :items="items" :loading="loading" />
  </div>
</template>

2. Feature Layer (Domain)

Setiap domain adalah bounded context yang berisi:
ComponentResponsibility
api/HTTP calls ke backend
components/UI components khusus domain
composables/Business logic dan page state
store/Global state management
types/TypeScript interfaces
index.tsBarrel export
// features/03_inventory/index.ts (Barrel Export)
export * from './api'
export * from './store'
export * from './composables'
export * from './types'
export { default as InventoryTable } from './components/InventoryTable.vue'

3. Data Layer

Menangani:
  • API communication
  • Data transformation
  • Caching (IndexedDB)
// features/03_inventory/api/getList.ts
export const getInventoryList = async (params: InventoryQueryParams) => {
  const query = new URLSearchParams()
  if (params.page) query.set('page', params.page.toString())
  if (params.limit) query.set('limit', params.limit.toString())

  return await $fetch<InventoryListResponse>('/api/inventory', { query })
}

4. Backend Layer (Nitro)

Server-side logic:
  • API proxying
  • Server middleware
  • SSR data fetching

Key Principles

1. Separation of Concerns

Setiap layer memiliki tanggung jawab yang jelas:
┌─────────────────────────────────────────────┐
│ UI Layer        → Render & User Interaction │
├─────────────────────────────────────────────┤
│ Composable      → Page Logic & Coordination │
├─────────────────────────────────────────────┤
│ Store           → State Management          │
├─────────────────────────────────────────────┤
│ API             → HTTP Communication        │
└─────────────────────────────────────────────┘

2. Single Responsibility

Setiap file memiliki satu alasan untuk berubah:
// BAD: Mixed responsibilities
const useInventory = () => {
  // State management
  const items = ref([])
  // API call
  const fetch = async () => { /* ... */ }
  // UI logic
  const formatPrice = (price) => { /* ... */ }
  // Validation
  const validate = (item) => { /* ... */ }
}

// GOOD: Separated responsibilities
// store/inventory.store.ts - State only
// api/getList.ts - API only
// composables/useInventoryList.ts - Page logic only
// utils/format.ts - Formatting only

3. Dependency Inversion

High-level modules tidak bergantung pada low-level modules:
// Composable depends on store interface, not implementation
const useInventoryList = () => {
  const store = useInventoryStore()  // Injected dependency

  const fetchItems = async () => {
    await store.fetchItems()  // Uses store abstraction
  }

  return { items: store.items, fetchItems }
}

4. DRY (Don’t Repeat Yourself)

Reuse components dan utilities:
// utils/format.ts - Reusable across all domains
export const formatCurrency = (value: number, currency = 'IDR') => {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency
  }).format(value)
}

// Used in multiple domains
import { formatCurrency } from '~/utils/format'

Feature Module Structure

Setiap feature module mengikuti struktur yang sama:
features/{domain}/
├── api/
│   ├── index.ts          # Barrel export
│   ├── getList.ts        # GET /api/{domain}
│   ├── getById.ts        # GET /api/{domain}/:id
│   ├── create.ts         # POST /api/{domain}
│   ├── update.ts         # PUT /api/{domain}/:id
│   └── delete.ts         # DELETE /api/{domain}/:id

├── components/
│   ├── index.ts          # Barrel export
│   ├── {Domain}Table.vue # List table
│   ├── {Domain}Form.vue  # Create/Edit form
│   └── {Domain}Card.vue  # Card component

├── composables/
│   ├── index.ts          # Barrel export
│   ├── use{Domain}List.ts    # List page logic
│   ├── use{Domain}Detail.ts  # Detail page logic
│   └── use{Domain}Form.ts    # Form logic

├── store/
│   ├── index.ts          # Barrel export
│   └── {domain}.store.ts # Pinia store

├── types/
│   ├── index.ts          # Barrel export
│   └── {domain}.types.ts # TypeScript interfaces

└── index.ts              # Main barrel export

Decision Matrix

SituationLocationReason
Shared across multiple pagesPinia StoreGlobal state persistence
Used in single page onlyComposablePage-specific logic
Used in single componentComponent refLocal state
API callsfeatures/*/api/Centralized, testable
Business logicStore or ComposableReusable
UI state (modals, etc.)Component refLocal, temporary
Utility functionsutils/Cross-domain reuse
Type definitionsfeatures/*/types/ or types/Type safety

Cross-Domain Communication

Ketika satu domain perlu mengakses domain lain:
// features/05_sales/store/sales.store.ts
import { useInventoryStore } from '~/features/03_inventory'

export const useSalesStore = defineStore('sales', () => {
  const inventoryStore = useInventoryStore()

  const createSale = async (items: SaleItem[]) => {
    // Check inventory availability
    const availableItems = items.filter(item =>
      inventoryStore.checkAvailability(item.productId, item.quantity)
    )

    if (availableItems.length !== items.length) {
      throw new Error('Some items are not available')
    }

    // Create sale
    // ...
  }

  return { createSale }
})

Adding New Feature

1

Create Feature Folder

mkdir -p features/new_feature/{api,components,composables,store,types}
2

Define Types

// features/new_feature/types/index.ts
export interface NewFeature {
  id: string
  name: string
  // ...
}
3

Create API Layer

// features/new_feature/api/getList.ts
export const getNewFeatureList = async () => {
  return await $fetch<NewFeature[]>('/api/new-feature')
}
4

Create Store

// features/new_feature/store/newFeature.store.ts
export const useNewFeatureStore = defineStore('newFeature', () => {
  const items = ref<NewFeature[]>([])
  // ...
  return { items }
})
5

Create Barrel Export

// features/new_feature/index.ts
export * from './api'
export * from './store'
export * from './composables'
export * from './types'

Best Practices

  • Store: use{Feature}Store (e.g., useInventoryStore)
  • Composable: use{Feature}{Action} (e.g., useInventoryList)
  • Component: {Feature}{Type} (e.g., InventoryTable)
  • API: {action}{Feature} (e.g., getInventoryList)
// Good - Use barrel exports
import { useInventoryStore, InventoryTable } from '~/features/03_inventory'

// Avoid - Direct imports
import { useInventoryStore } from '~/features/03_inventory/store/inventory.store'
  • Selalu define types di types/ folder
  • Hindari any type
  • Export types dari barrel export
  • Use strict TypeScript mode

Next Steps