| name | architecture |
| description | Use when organizing code or making architectural decisions. Triggers on "barrel export", "index.ts", "file structure", "naming convention", "module boundary", "dependency injection", "InjectionToken", "scalability", or organization questions. |
| allowed-tools | ["Read","Glob","Grep"] |
Architecture: Scalability & Maintainability Principles
This library must be SCALABLE and MAINTAINABLE for long-term success.
Scalability Principles
1. Component Composition Over Inheritance
- Use
hostDirectives for cross-cutting concerns
- Prefer composition patterns over complex inheritance hierarchies
- Keep components small and focused (single responsibility)
Example:
@Component({
selector: 'ai-copyable-code-block',
hostDirectives: [
{
directive: CopyToClipboardDirective,
inputs: ['text'],
},
],
})
export class CopyableCodeBlockComponent {}
class BaseComponent {}
class InteractiveComponent extends BaseComponent {}
class CopyableComponent extends InteractiveComponent {}
2. Module Boundaries & Dependency Management
- Enforce strict module boundaries in Nx
- Prevent circular dependencies
- Use DI tokens for extensibility (don't hardcode implementations)
- Keep libraries loosely coupled
Example:
import { SpecificChatService } from '../../../services/specific-chat.service';
export const CHAT_CONFIG = new InjectionToken<ChatConfig>('ChatConfig');
@Component({
selector: 'ai-chat',
})
export class ChatComponent {
private config = inject(CHAT_CONFIG);
}
3. Performance from Day One
- OnPush change detection everywhere
- Minimize signal computations (avoid nested computed signals)
- Lazy load when possible
- Tree-shakable exports (use barrel exports carefully)
Example:
containerClasses = computed(() => {
return cn('base-class', { active: this.isActive() });
});
containerClasses = computed(() => {
const nestedComputed = computed(() => this.isActive());
return cn('base-class', { active: nestedComputed() });
});
4. API Design
- Design public APIs with backwards compatibility in mind
- Use semantic versioning
- Deprecate gracefully (don't break existing users)
- Keep internal APIs private (use TypeScript private/protected)
Maintainability Principles
1. Code Organization
- Maximum 500 lines per file (refactor if larger)
- Group related functionality into feature folders
- Clear separation: components / directives / services / types / utils
- Consistent file naming:
feature-name.component.ts, feature-name.service.ts
File Structure:
message-bubble/
message-bubble.component.ts (max 500 lines)
message-bubble.types.ts
message-bubble-helpers.ts
index.ts
2. Type Safety
- TypeScript strict mode enabled
- No
any types (use unknown if truly needed)
- Use discriminated unions for complex types
- Define clear interfaces for all public APIs
3. Documentation
- JSDoc comments for all public APIs
- Include usage examples in comments
- Document complex logic with inline comments
- Keep README and PLAN.md up to date
4. Consistency
- Follow established patterns (don't invent new ones unnecessarily)
- Use shared utilities from
@angular-ai-kit/utils
- Consistent naming conventions throughout
- Automated formatting with Prettier
5. Error Handling
- Fail gracefully with user-friendly messages
- Log errors appropriately (but remove before production)
- Provide fallback UI states
- Don't swallow errors silently
Example:
async function loadMessages() {
try {
const messages = await this.chatService.getMessages();
this.messages.set(messages);
} catch (error) {
console.error('Failed to load messages:', error);
this.error.set('Unable to load messages. Please try again.');
this.showErrorState.set(true);
}
}
Dependency Injection Patterns
Service Injection
export class ChatComponent {
private chatService = inject(ChatService);
private config = inject(CHAT_CONFIG);
}
export class ChatComponent {
constructor(private chatService: ChatService) {}
}
Optional Dependencies
export class ChatComponent {
private analytics = inject(AnalyticsService, { optional: true });
logEvent(event: string) {
this.analytics?.trackEvent(event);
}
}
Self Injection (for host directive access)
export class ChatComponent {
private self = inject(ElementRef);
private host = inject(HostComponent, { optional: true, host: true });
}
Barrel Exports (CRITICAL)
ALWAYS create an index.ts file in every folder that contains components, directives, services, or types.
Why Barrel Exports?
- Clean, organized imports for consumers
- Single entry point for each module
- Easy refactoring without breaking imports
- Tree-shaking support
Rules
- Every component/directive/service folder MUST have an
index.ts
- Export only public API - don't export internal helpers
- Use named exports - avoid default exports
- Re-export from parent index.ts - create a hierarchy
Examples
export { MessageBubbleComponent } from './message-bubble.component';
export type { MessageBubbleConfig } from './message-bubble.types';
export * from './message-bubble';
export * from './message-list';
export * from './chat-container';
export * from './chat';
export * from './input';
export * from './display';
export * from './lib/components';
export * from './lib/directives';
export * from './lib/types';
export * from './internal-helper';
Folder Structure
message-bubble/
message-bubble.component.ts
message-bubble.component.html
message-bubble.types.ts (optional)
index.ts <- REQUIRED
Naming Conventions
Files
- Components:
kebab-case.component.ts (e.g., message-bubble.component.ts)
- Templates:
kebab-case.component.html (e.g., message-bubble.component.html)
- Directives:
kebab-case.directive.ts (e.g., copy-to-clipboard.directive.ts)
- Services:
kebab-case.service.ts (e.g., chat.service.ts)
- Types:
kebab-case.types.ts (e.g., chat-message.types.ts)
- Utils:
kebab-case.ts (e.g., token-counter.ts)
- Barrel exports:
index.ts in each folder
Classes & Interfaces
- Components:
PascalCase + Component suffix (e.g., MessageBubbleComponent)
- Directives:
PascalCase + Directive suffix (e.g., CopyToClipboardDirective)
- Services:
PascalCase + Service suffix (e.g., ChatService)
- Interfaces:
PascalCase (e.g., ChatMessage, MessageRole)
Selectors
- Components:
ai-kebab-case prefix (e.g., ai-message-bubble)
- Directives:
aiCamelCase prefix (e.g., aiCopyToClipboard)
File Size Limits
- Maximum 500 lines per file
- If a file exceeds 500 lines, refactor by:
- Extracting helper functions to separate files
- Splitting complex components into smaller ones
- Moving types to
.types.ts files
- Creating utility functions in
@angular-ai-kit/utils
Component Communication Patterns
Parent to Child: Inputs
@Component({
selector: 'ai-child',
})
export class ChildComponent {
data = input.required<string>();
}
Child to Parent: Outputs
@Component({
selector: 'ai-child',
})
export class ChildComponent {
action = output<string>();
emitAction() {
this.action.emit('data');
}
}
Sibling Communication: Service
@Injectable({ providedIn: 'root' })
export class SharedStateService {
private state = signal<string>('');
readonly state$ = this.state.asReadonly();
updateState(value: string) {
this.state.set(value);
}
}
Cross-Component: Injection Tokens
export const CHAT_CONTEXT = new InjectionToken<ChatContext>('ChatContext');
@Component({
selector: 'ai-chat-provider',
providers: [
{
provide: CHAT_CONTEXT,
useValue: { conversationId: '123' },
},
],
})
export class ChatProviderComponent {}
@Component({
selector: 'ai-message',
})
export class MessageComponent {
private context = inject(CHAT_CONTEXT);
}
Lazy Loading Pattern
export const routes: Routes = [
{
path: 'chat',
loadComponent: () =>
import('./chat/chat.component').then((m) => m.ChatComponent),
},
];
Performance Optimization Checklist