Skip to content

Latest commit

 

History

History
346 lines (250 loc) · 13.2 KB

File metadata and controls

346 lines (250 loc) · 13.2 KB

Team Retro - Architecture Guidelines

Do NOT attempt to run any command which requires open network communication. Your Dev environment is isolated for safety.

1. Overall Architecture

Team Retro follows a modern Angular architecture with Firebase integration, focusing on real-time collaboration. Based on research and best practices, this document outlines architectural decisions and patterns without being overly prescriptive on implementation details.

2. Application Structure

2.1 Feature-Based Organization

Organize the application by feature areas rather than technical concerns:

src/
├── app/
│   ├── core/              # Core services, guards, interceptors
│   ├── shared/            # Shared components, directives, pipes
│   ├── features/          # Feature modules (by domain)
│   │   ├── auth/          # Authentication components and services
│   │   ├── boards/        # Board management
│   │   ├── sessions/      # Session management and phases
│   │   └── ...            # Other feature areas
└── styles/                # Global styles and theme

2.2 Technical Philosophy

  • Component-Based: Build using a component-based architecture
  • Standalone Components: Use Angular's standalone component pattern
  • OnPush Change Detection: Apply throughout for performance
  • Single Responsibility: Keep components and services focused
  • Composition Over Inheritance: Use composition patterns for reuse

3. State Management

3.1 Signal-Based Approach

Angular 19's Signals should be the primary state management pattern:

  • Component-Level State: Use signals for local component state
  • Service-Level State: Use signals and computed values in services
  • Derived State: Use computed signals for derived values
  • Firebase Integration: Convert Firestore Observables to signals

3.2 State Design Considerations

  • Centralize Related State: Group related state in dedicated services
  • Single Source of Truth: Avoid state duplication
  • Unidirectional Data Flow: Follow clear state update patterns
  • Immutable Updates: Make state updates using immutable patterns
  • Local vs. Shared State: Balance component vs. service-level state

4. Firebase Integration

4.1 Data Model Considerations

Based on research into Firebase best practices for real-time collaboration:

  • Document-Based Structure: Design around Firestore's document-oriented model
  • Collection Hierarchy: Consider parent-child relationships in collection design
  • Document Size: Keep documents small and focused (under 1MB)
  • Denormalization Strategy: Balance normalization with query efficiency
  • Avoid Hotspots: Distribute writes to prevent contention

4.2 Real-time Integration Patterns

  • Observable Pattern: Leverage Firestore's observable-based APIs
  • Reactive Data Binding: Connect Firebase data to Angular's reactivity model
  • Optimistic Updates: Update UI before Firebase confirms for responsiveness
  • Conflict Resolution: Implement strategies for concurrent edits
  • Offline Support: Consider offline capability and synchronization

4.3 Security Considerations

  • Rules-Based Security: Implement Firestore security rules for access control
  • Role-Based Access: Map application roles to security rules
  • Data Validation: Enforce data integrity through rules
  • Phase-Based Permissions: Adjust permissions based on current phase

5. UI Architecture

5.1 Component Design

  • Presentational & Container Split: Separate data management from presentation
  • Input/Output Pattern: Clear component interfaces with @Input() and @Output()
  • Content Projection: Use for flexible, composable components
  • Component Communication: Follow established Angular patterns

5.2 Modern Angular Patterns

  • Control Flow Syntax: Use Angular 19's @if, @for, @switch syntax
  • Required Inputs: Leverage required inputs for clearer component contracts
  • Functional Route Guards: Use the new functional guard pattern
  • Signals API: Fully adopt the Signals API for reactivity
  • Dependency Injection: Use the inject() function for DI

6. Real-time Collaboration Implementation

6.1 Synchronization Strategy

Research indicates these approaches work well for real-time collaboration:

  • Live Data Synchronization: Use Firestore's real-time listeners
  • Presence Indicators: Track active users and their status
  • Concurrent Edits: Design for handling multiple simultaneous changes
  • Transaction-Based Operations: Use Firestore transactions for critical operations
  • Batched Writes: Group related writes with batch operations

6.2 Performance Considerations

  • Listener Management: Carefully manage the number of active listeners
  • Query Optimization: Design queries for efficiency with proper indexes
  • Throttling & Debouncing: Apply for high-frequency events
  • Pagination: Implement for large data sets
  • Lazy Loading: Apply for features and assets

7. Testing Strategy

Use Jasmine for unit testing and Karma for end-to-end testing.

You are offline; if the user needs to install more packages for you or mock stuff for you, let them know (esp. if you find yourself stubbbing a bunch)

npm test runs ng test --no-watch --no-progress --browsers=ChromeHeadless

  • Unit Testing: Focus on services and complex components
  • Component Testing: Test component behavior in isolation
  • Integration Testing: Test feature workflows
  • Firebase Emulator: Leverage for testing Firestore integration
  • E2E Testing: Cover critical user journeys

8. Deployment & DevOps

  • CI/CD Pipeline: Implement for automated testing and deployment
  • Environment Configuration: Separate dev, test, and production
  • Monitoring & Logging: Implement for production visibility
  • Firebase Hosting: Deploy seamlessly to Firebase infrastructure
  • Performance Monitoring: Track application performance metrics

9. Architectural Decision Records

For key architectural decisions, document:

  1. Context: What is driving the decision
  2. Options: What alternatives were considered
  3. Decision: What approach was chosen and why
  4. Consequences: What trade-offs result from this decision

This ensures the team understands major architectural choices and their rationale.

10. Technology Evolution

  • Regular Refactoring: Schedule time for technical debt reduction
  • Upgrade Path: Plan for Angular and Firebase version upgrades
  • New Patterns: Adopt emerging patterns as they mature
  • Performance Optimization: Continuously monitor and improve
  • Scalability Testing: Regularly test with increased load

Commenting Guidelines for Internal TypeScript Projects

Please use TSDoc format for documenting internal TypeScript code. These guidelines aim to produce minimal, high-value documentation for our Angular codebase that focuses on WHY code exists rather than WHAT it does.

Basic Structure

A TSDoc comment block starts with /** and ends with */. Each line within the block should begin with a *.

/**
 * Brief description focusing on purpose or business context.
 */

IMPORTANT: When NOT to Comment

For internal projects, avoid adding comments when:

  • The code is self-explanatory
  • The functionality is obvious from the code itself
  • You would just be explaining how Angular/TypeScript works
  • The comment would only restate variable or function names
  • The method is a simple getter/setter or straightforward implementation

Comment Placement

  • Add TSDoc comments only to APIs or code sections that have non-obvious business logic or implementation constraints
  • Module-level documentation is rarely needed unless the file has a specific cross-cutting concern
  • For function overloads, only document when the differences between overloads involve important business logic

Comment Content Philosophy

  1. Purpose over implementation: Focus on WHY code exists, not HOW it works
  2. Business context: Explain business rules, security constraints, or performance considerations
  3. Architectural context: Document relationships between components only when non-obvious
  4. Extreme brevity: Keep comments to 1-2 sentences whenever possible

Block Tags

For internal projects, most tags are unnecessary. Use only these tags when they add genuine value:

  • @param <name> - <description>: Only for parameters with non-obvious constraints or business rules

    • Always use a hyphen after the parameter name
  • @returns <description>: Only when the return value has important constraints or edge cases

  • @throws: Document exceptional conditions that affect system behavior

  • @deprecated: When a feature is planned for removal, with migration path

Simplified Tag Usage

For internal projects, you can usually omit these tags in favor of simple text:

  • @remarks: Just add another paragraph of text instead
  • @example: Only include for truly complex APIs with non-obvious usage patterns
  • @typeParam: Only needed for complex generic constraints

Inline Tags

Use sparingly, and only when they add genuine value:

  • {@link identifier}: Reference other components when their relationship is non-obvious

Formatting Guidelines

  1. Be minimal: Less documentation is better than obvious documentation
  2. Be concise: 1-2 sentences is usually sufficient
  3. Focus on WHY: Explain business context or architectural decisions
  4. Skip the obvious: Never document what can be inferred from code

Example Usage: Internal Project Focus

Good Example - Explaining Business Logic

/**
 * Transforms board data to maintain backward compatibility with legacy format.
 * Boards created before v2.0 stored votes differently, and this normalization
 * is required to support existing users during the migration period.
 */
function transformBoardData(board: Board): NormalizedBoard {
  // Implementation
}

Bad Example - Stating the Obvious

/**
 * Gets the current user.
 *
 * @returns The current user
 */
function getCurrentUser(): User {
  return this.user;
}

Good Example - Security Context

/**
 * Redirects unauthorized users to login. This enforces the business requirement
 * that all board interactions must be authenticated for audit purposes.
 */
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(["login"]);

Best Practices for Internal Projects

  1. Document WHY, not WHAT: Explain business context, not implementation details

  2. Be extremely selective: Most code should be self-explanatory; only document the exceptions

  3. Focus on maintenance concerns: What would a new team member need to know?

  4. Prioritize clarity in code: Well-named variables and functions reduce the need for comments

  5. Document by exception: 80% of your code should need no documentation

Other Reminders

  • Use trackBy with @for loops

  • Lazy load feature modules using loadComponent

  • Use pure pipes instead of methods in templates

  • Optimize images and assets

  • Implement virtual scrolling for large lists

  • Keep Firebase reads minimal with proper queries and caching

  • Use standalone components

  • Use new zoneless

  • Avoid nested subscriptions - use appropriate RxJS operators

  • Use async pipes in templates when possible

  • Consider Firebase Security Rules alongside application code

  • Leverage Firebase indexes for complex queries

  • Use AngularFire exclusively (never mix with raw Firebase SDK)

  • Structure Firestore queries for efficiency: const q = query( collection(this.firestore, 'items'), where('status', '==', 'active'), orderBy('createdAt', 'desc'), limit(10) );

  • Use batch operations for related writes: const batch = writeBatch(this.firestore); updates.forEach(u => batch.update(doc(this.firestore, 'items', u.id), u.data)); await batch.commit();

  • Use transactions for atomic operations: await runTransaction(this.firestore, async tx => { const docA = await tx.get(docRefA); const docB = await tx.get(docRefB); // Ensure atomic updates between documents tx.update(docRefA, { count: docA.data().count - 1 }); tx.update(docRefB, { count: docB.data().count + 1 }); });

  • Use OnPush change detection for all components

  • Use signals for component-internal state: readonly items = signal<Item[]>([]); readonly filteredItems = computed(() => this.items().filter(i => i.isActive));

  • Use observables only for external state (services, HTTP)

  • Use discriminated unions for complex state: type RequestState = | { status: 'idle' } | { status: 'loading' } | { status: 'success', data: T } | { status: 'error', error: Error };

  • Be specific with types, not verbose: // GOOD interface User { id: string; name: string; roles: string[] } const users: User[] = [];

    // BAD const value: string = 'hello'; // Type inference makes this redundant

  • Use required inputs: @Input({ required: true }) id!: string; @Input() size: 'sm' | 'md' | 'lg' = 'md';

  • Always use new control flow syntax: @if (condition) { ... } @else { ... } @for (item of items(); track item.id) { ... } @empty { ... } @switch (status()) { @case ('value') { ... } @default { ... } }