# 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(null); login(credentials: Credentials): Observable { return this.http.post('/api/auth/login', credentials).pipe( tap(user => this.currentUser$.next(user)) ); } getCurrentUser(): Observable { 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: ` @for (column of columns(); track column.key) { } @for (row of data(); track row.id) { @for (column of columns(); track column.key) { } }
{{ column.label }}
{{ row[column.key] }}
` }) export class DataTableComponent { columns = input.required(); data = input.required(); } ``` ### 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: ` @if (loading()) { } @else if (error()) { } @else { @for (product of products(); track product.id) { } } ` }) export class ProductListComponent { private productService = inject(ProductService); products = signal([]); loading = signal(false); error = signal(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: `

{{ product().name }}

{{ product().price | currency }}

` }) export class ProductCardComponent { product = input.required(); edit = output(); delete = output(); } ``` --- ## 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(endpoint: string): Observable { return this.http.get(`${this.baseUrl}/${endpoint}`); } post(endpoint: string, data: any): Observable { return this.http.post(`${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 { return this.api.get('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([]); // 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)) ); addItem(item: CartItem) { const current = this.itemsSubject.value; this.itemsSubject.next([...current, item]); } removeItem(id: string) { const current = this.itemsSubject.value; this.itemsSubject.next(current.filter(item => item.id !== id)); } clear() { this.itemsSubject.next([]); } } ``` --- ## Pattern 4: Facade Pattern ### Overview Create a single entry point for complex subsystems. ### Use Case When a feature has multiple related services that components need to interact with. **Example:** ```typescript // features/checkout/services/checkout.facade.ts @Injectable({ providedIn: 'root' }) export class CheckoutFacade { private cartService = inject(CartService); private paymentService = inject(PaymentService); private shippingService = inject(ShippingService); private orderService = inject(OrderService); // Expose simplified API cart$ = this.cartService.items$; total$ = this.cartService.total$; shippingMethods$ = this.shippingService.getMethods(); processCheckout(data: CheckoutData): Observable { return this.validateCart().pipe( switchMap(() => this.calculateShipping(data.shippingMethod)), switchMap(shipping => this.processPayment(data.payment, shipping)), switchMap(payment => this.createOrder({ ...data, payment })), tap(() => this.cartService.clear()) ); } private validateCart(): Observable { return this.cart$.pipe( take(1), map(items => items.length > 0), tap(valid => { if (!valid) throw new Error('Cart is empty'); }) ); } private calculateShipping(method: string): Observable { return this.shippingService.calculate(method); } private processPayment(payment: PaymentInfo, shipping: number): Observable { return this.total$.pipe( take(1), switchMap(total => this.paymentService.charge({ ...payment, amount: total + shipping })) ); } private createOrder(data: OrderData): Observable { return this.orderService.create(data); } } // Component uses facade instead of multiple services @Component({...}) export class CheckoutComponent { private facade = inject(CheckoutFacade); cart$ = this.facade.cart$; total$ = this.facade.total$; checkout(data: CheckoutData) { this.facade.processCheckout(data).subscribe({ next: order => this.router.navigate(['/order-confirmation', order.id]), error: err => this.showError(err) }); } } ``` --- ## Pattern 5: Error Handling Strategy ### Global Error Handler ```typescript // core/handlers/global-error.handler.ts @Injectable() export class GlobalErrorHandler implements ErrorHandler { private logger = inject(LoggerService); private notification = inject(NotificationService); handleError(error: Error | HttpErrorResponse) { if (error instanceof HttpErrorResponse) { // Server error this.handleHttpError(error); } else { // Client error this.handleClientError(error); } } private handleHttpError(error: HttpErrorResponse) { const message = this.getErrorMessage(error); this.notification.showError(message); this.logger.error('HTTP Error', { error, url: error.url }); } private handleClientError(error: Error) { this.notification.showError('An unexpected error occurred'); this.logger.error('Client Error', { error, stack: error.stack }); } private getErrorMessage(error: HttpErrorResponse): string { if (error.status === 0) { return 'No internet connection'; } else if (error.status === 401) { return 'Session expired. Please login again.'; } else if (error.status === 403) { return 'You do not have permission to perform this action'; } else if (error.status >= 500) { return 'Server error. Please try again later.'; } return error.error?.message || 'An error occurred'; } } ``` ### HTTP Error Interceptor ```typescript // core/interceptors/error.interceptor.ts export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Redirect to login inject(Router).navigate(['/login']); } return throwError(() => error); }) ); }; ``` --- ## Pattern 6: Feature Flags ### Overview Control feature visibility without deploying new code. ```typescript // core/services/feature-flag.service.ts @Injectable({ providedIn: 'root' }) export class FeatureFlagService { private flags = signal>({ 'new-dashboard': false, 'beta-checkout': true, 'admin-analytics': false }); isEnabled(feature: string): boolean { return this.flags()[feature] ?? false; } enable(feature: string) { this.flags.update(flags => ({ ...flags, [feature]: true })); } disable(feature: string) { this.flags.update(flags => ({ ...flags, [feature]: false })); } } // Usage in component @Component({ template: ` @if (showNewDashboard()) { } @else { } ` }) export class DashboardComponent { private featureFlags = inject(FeatureFlagService); showNewDashboard = computed(() => this.featureFlags.isEnabled('new-dashboard')); } // Usage in routes { path: 'beta', loadComponent: () => import('./beta.component'), canActivate: [() => inject(FeatureFlagService).isEnabled('beta-features')] } ``` --- ## Pattern 7: Caching Strategy ### Service-Level Cache ```typescript // core/services/cache.service.ts @Injectable({ providedIn: 'root' }) export class CacheService { private cache = new Map(); private TTL = 5 * 60 * 1000; // 5 minutes get(key: string): T | null { const cached = this.cache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > this.TTL) { this.cache.delete(key); return null; } return cached.data as T; } set(key: string, data: any) { this.cache.set(key, { data, timestamp: Date.now() }); } clear(key?: string) { if (key) { this.cache.delete(key); } else { this.cache.clear(); } } } // Usage in service @Injectable({ providedIn: 'root' }) export class ProductService { private cache = inject(CacheService); private api = inject(ApiService); getProducts(): Observable { const cached = this.cache.get('products'); if (cached) { return of(cached); } return this.api.get('products').pipe( tap(products => this.cache.set('products', products)) ); } } ``` --- ## Pattern 8: Loading States ### Unified Loading Pattern ```typescript // core/models/loading-state.interface.ts export interface LoadingState { loading: boolean; data: T | null; error: string | null; } // Feature service @Injectable({ providedIn: 'root' }) export class ProductService { private state = signal>({ loading: false, data: null, error: null }); state$ = computed(() => this.state()); loadProducts() { this.state.update(s => ({ ...s, loading: true, error: null })); this.api.get('products').subscribe({ next: data => this.state.set({ loading: false, data, error: null }), error: err => this.state.set({ loading: false, data: null, error: err.message }) }); } } // Component @Component({ template: ` @if (state().loading) { } @else if (state().error) { } @else if (state().data) { @for (product of state().data; track product.id) { } } ` }) export class ProductListComponent { private service = inject(ProductService); state = this.service.state$; ngOnInit() { this.service.loadProducts(); } } ``` --- ## Team Structure Patterns ### Pattern 1: Feature Teams - Each team owns complete features - Vertical slice (UI + API + DB) - Autonomous deployment - Best for: Medium to large teams (10-50+) ### Pattern 2: Layer Teams - Frontend team, backend team - Horizontal slice - Coordinated deployment - Best for: Small teams (5-10) ### Pattern 3: Component Teams - Shared component library team - Feature teams consume components - Hybrid approach - Best for: Large organizations (50+) --- ## Summary Enterprise patterns ensure: - ✅ Consistent codebase across large teams - ✅ Predictable structure for new developers - ✅ Separation of concerns - ✅ Testable, maintainable code - ✅ Scalability from day one **Key Takeaway:** Patterns create consistency. Consistency enables scale.