一键导入
enterprise-patterns
Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | enterprise-patterns |
| description | Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams |
Proven architectural patterns for building scalable Angular applications in enterprise environments with teams of 5-100+ developers.
Organize code into three main categories based on scope and reusability.
src/app/
├── core/ # App-wide singletons (loaded once)
├── shared/ # Reusable components/utilities
└── features/ # Feature modules (lazy loaded)
What belongs in core:
What does NOT belong:
Example:
// 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();
}
}
What belongs in shared:
What does NOT belong:
Example:
// 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[]>();
}
What belongs in features:
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
Separate components that manage data (smart) from components that display data (dumb).
Characteristics:
Example:
// 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();
}
}
}
Characteristics:
Example:
// 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>();
}
Organize services by responsibility: data access, business logic, and state management.
Purpose: HTTP communication only
// 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);
}
}
Purpose: Business logic and domain operations
// 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);
}
}
Purpose: Manage application state
// 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))
);
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([]);
}
}
Create a single entry point for complex subsystems.
When a feature has multiple related services that components need to interact with.
Example:
// 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<Order> {
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<boolean> {
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<number> {
return this.shippingService.calculate(method);
}
private processPayment(payment: PaymentInfo, shipping: number): Observable<PaymentResult> {
return this.total$.pipe(
take(1),
switchMap(total => this.paymentService.charge({
...payment,
amount: total + shipping
}))
);
}
private createOrder(data: OrderData): Observable<Order> {
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)
});
}
}
// 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';
}
}
// 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);
})
);
};
Control feature visibility without deploying new code.
// core/services/feature-flag.service.ts
@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
private flags = signal<Record<string, boolean>>({
'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()) {
<app-new-dashboard />
} @else {
<app-old-dashboard />
}
`
})
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')]
}
// core/services/cache.service.ts
@Injectable({ providedIn: 'root' })
export class CacheService {
private cache = new Map<string, { data: any; timestamp: number }>();
private TTL = 5 * 60 * 1000; // 5 minutes
get<T>(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<Product[]> {
const cached = this.cache.get<Product[]>('products');
if (cached) {
return of(cached);
}
return this.api.get<Product[]>('products').pipe(
tap(products => this.cache.set('products', products))
);
}
}
// core/models/loading-state.interface.ts
export interface LoadingState<T> {
loading: boolean;
data: T | null;
error: string | null;
}
// Feature service
@Injectable({ providedIn: 'root' })
export class ProductService {
private state = signal<LoadingState<Product[]>>({
loading: false,
data: null,
error: null
});
state$ = computed(() => this.state());
loadProducts() {
this.state.update(s => ({ ...s, loading: true, error: null }));
this.api.get<Product[]>('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) {
<app-loading-spinner />
} @else if (state().error) {
<app-error-message [message]="state().error" />
} @else if (state().data) {
@for (product of state().data; track product.id) {
<app-product-card [product]="product" />
}
}
`
})
export class ProductListComponent {
private service = inject(ProductService);
state = this.service.state$;
ngOnInit() {
this.service.loadProducts();
}
}
Enterprise patterns ensure:
Key Takeaway: Patterns create consistency. Consistency enables scale.
Doguhan Uluca's Router-First Architecture - The 7 steps for designing scalable Angular applications by defining routes before components
Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators
Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS
Comprehensive guide to Angular bundle optimization, code splitting, tree shaking, and lazy loading strategies
Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns
Secure authentication and authorization patterns including JWT, OAuth2, guards, and role-based access control