Claude
Skills
Sign in
Back

enterprise-patterns

Included with Lifetime
$97 forever

Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams

General

What this skill does


# Enterprise Angular Patterns

Proven architectural patterns for building scalable Angular applications in enterprise environments with teams of 5-100+ developers.

---

## Core Principles

1. **Separation of Concerns** - Each piece of code has one responsibility
2. **Single Source of Truth** - State lives in one place
3. **Consistency** - Follow patterns religiously
4. **Scalability** - Design for 10x growth
5. **Maintainability** - Code should be easy to change

---

## Pattern 1: Core-Shared-Features Structure

### Overview
Organize code into three main categories based on scope and reusability.

### The Three Folders

```
src/app/
├── core/          # App-wide singletons (loaded once)
├── shared/        # Reusable components/utilities
└── features/      # Feature modules (lazy loaded)
```

### Core Module Rules

**What belongs in core:**
- ✅ Singleton services (AuthService, ApiService, CacheService)
- ✅ HTTP interceptors (auth, error handling, retry)
- ✅ Route guards (authentication, authorization)
- ✅ Global error handlers
- ✅ App-wide models and interfaces
- ✅ Constants and configuration

**What does NOT belong:**
- ❌ UI components
- ❌ Feature-specific services
- ❌ Reusable utilities (those go in shared)

**Example:**
```typescript
// core/services/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private currentUser$ = new BehaviorSubject<User | null>(null);
  
  login(credentials: Credentials): Observable<User> {
    return this.http.post<User>('/api/auth/login', credentials).pipe(
      tap(user => this.currentUser$.next(user))
    );
  }
  
  getCurrentUser(): Observable<User | null> {
    return this.currentUser$.asObservable();
  }
}
```

### Shared Module Rules

**What belongs in shared:**
- ✅ Dumb/presentational components (buttons, cards, modals)
- ✅ Custom directives (tooltips, permissions, auto-focus)
- ✅ Custom pipes (formatting, filtering)
- ✅ Utility functions (date helpers, validators)
- ✅ Common interfaces used across features

**What does NOT belong:**
- ❌ Business logic
- ❌ HTTP calls
- ❌ Feature-specific components

**Example:**
```typescript
// shared/components/data-table/data-table.component.ts
@Component({
  selector: 'app-data-table',
  standalone: true,
  template: `
    <table>
      <thead>
        <tr>
          @for (column of columns(); track column.key) {
            <th>{{ column.label }}</th>
          }
        </tr>
      </thead>
      <tbody>
        @for (row of data(); track row.id) {
          <tr>
            @for (column of columns(); track column.key) {
              <td>{{ row[column.key] }}</td>
            }
          </tr>
        }
      </tbody>
    </table>
  `
})
export class DataTableComponent {
  columns = input.required<Column[]>();
  data = input.required<any[]>();
}
```

### Features Module Rules

**What belongs in features:**
- ✅ Feature-specific components (smart + dumb)
- ✅ Feature-specific services
- ✅ Feature-specific models
- ✅ Feature routing configuration

**Structure:**
```
features/
└── products/
    ├── components/           # Feature components
    │   ├── product-list/
    │   ├── product-detail/
    │   └── product-form/
    ├── services/             # Feature services
    │   └── product.service.ts
    ├── models/               # Feature models
    │   └── product.interface.ts
    ├── products.routes.ts    # Feature routes
    └── products.component.ts # Container component
```

---

## Pattern 2: Smart and Dumb Components

### Overview
Separate components that manage data (smart) from components that display data (dumb).

### Smart Components (Containers)

**Characteristics:**
- Communicate with services
- Manage state
- Handle business logic
- Usually top-level feature components

**Example:**
```typescript
// features/products/product-list.component.ts
@Component({
  selector: 'app-product-list',
  template: `
    <app-search-bar (search)="handleSearch($event)" />
    
    @if (loading()) {
      <app-loading-spinner />
    } @else if (error()) {
      <app-error-message [error]="error()" />
    } @else {
      @for (product of products(); track product.id) {
        <app-product-card 
          [product]="product"
          (edit)="handleEdit($event)"
          (delete)="handleDelete($event)"
        />
      }
    }
  `
})
export class ProductListComponent {
  private productService = inject(ProductService);
  
  products = signal<Product[]>([]);
  loading = signal(false);
  error = signal<string | null>(null);
  
  ngOnInit() {
    this.loadProducts();
  }
  
  loadProducts() {
    this.loading.set(true);
    this.productService.getProducts().pipe(
      takeUntilDestroyed()
    ).subscribe({
      next: products => {
        this.products.set(products);
        this.loading.set(false);
      },
      error: err => {
        this.error.set(err.message);
        this.loading.set(false);
      }
    });
  }
  
  handleEdit(id: string) {
    this.router.navigate(['/products', id, 'edit']);
  }
  
  handleDelete(id: string) {
    if (confirm('Delete this product?')) {
      this.productService.delete(id).subscribe();
    }
  }
}
```

### Dumb Components (Presentational)

**Characteristics:**
- Receive data via @Input or input()
- Emit events via @Output or output()
- No service dependencies
- Highly reusable
- Easy to test

**Example:**
```typescript
// shared/components/product-card.component.ts
@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [CurrencyPipe],
  template: `
    <div class="card">
      <img [src]="product().image" [alt]="product().name" />
      <h3>{{ product().name }}</h3>
      <p>{{ product().price | currency }}</p>
      <div class="actions">
        <button (click)="edit.emit(product().id)">Edit</button>
        <button (click)="delete.emit(product().id)">Delete</button>
      </div>
    </div>
  `
})
export class ProductCardComponent {
  product = input.required<Product>();
  edit = output<string>();
  delete = output<string>();
}
```

---

## Pattern 3: Service Layer Architecture

### Overview
Organize services by responsibility: data access, business logic, and state management.

### Data Services

**Purpose:** HTTP communication only

```typescript
// core/services/api.service.ts
@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  private baseUrl = environment.apiUrl;
  
  get<T>(endpoint: string): Observable<T> {
    return this.http.get<T>(`${this.baseUrl}/${endpoint}`);
  }
  
  post<T>(endpoint: string, data: any): Observable<T> {
    return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data);
  }
}
```

### Business Services

**Purpose:** Business logic and domain operations

```typescript
// features/products/services/product.service.ts
@Injectable({ providedIn: 'root' })
export class ProductService {
  private api = inject(ApiService);
  
  getProducts(): Observable<Product[]> {
    return this.api.get<Product[]>('products').pipe(
      map(products => this.enrichProducts(products))
    );
  }
  
  private enrichProducts(products: Product[]): Product[] {
    return products.map(p => ({
      ...p,
      displayPrice: this.formatPrice(p.price),
      inStock: p.quantity > 0
    }));
  }
  
  private formatPrice(price: number): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(price);
  }
}
```

### State Services

**Purpose:** Manage application state

```typescript
// features/cart/services/cart-state.service.ts
@Injectable({ providedIn: 'root' })
export class CartStateService {
  private itemsSubject = new BehaviorSubject<CartItem[]>([]);
  
  // Public observable
  items$ = this.itemsSubject.asObservable();
  
  // Computed values
  total$ = this.items$.pipe(
    map(items => items.reduce((sum, item) => sum + item.price * item.quantity, 0))
  );
  
  itemCount$ = this.items$.pipe(
    map(items => items.reduce((count, item) => count + item.quantity, 0))
  );
  
  

Related in General