Skip to main content
Already have an existing project? This guide walks you through migrating to the Scope Rule architecture with real strategies and before/after examples.

Migration Strategy

Migration should be incremental and safe. You don’t need to restructure everything at once.

Phase 1: Audit Current Structure

1

Identify All Components

Create an inventory of every component, service, hook, and utility:
# Find all components
find src -name "*.tsx" -o -name "*.ts" -o -name "*.astro" | sort

# Or use a more sophisticated approach
tree src/components
tree src/features
Create a spreadsheet or document listing:
  • Component name
  • Current location
  • Files that import it
  • Number of features that use it
2

Map Feature Boundaries

Identify distinct business features in your application:Example mapping:
Current structure:          Feature identification:
/components                 
  /auth                     → Auth feature
  /products                 → Shop feature (products)
  /cart                     → Shop feature (cart)
  /profile                  → Profile feature
  /ui                       → Shared components?
Group related functionality:
  • Authentication (login, register, password reset)
  • Shop (products, cart, checkout, wishlist)
  • Profile (settings, preferences, history)
  • Dashboard (analytics, reports)
3

Count Component Usage

For each component, count how many distinct features use it:
# Find all imports of a component
grep -r "import.*ProductCard" src/

# Results analysis:
# src/features/shop/products/page.tsx      → shop feature
# src/features/shop/cart/page.tsx          → shop feature (same feature)
# src/features/wishlist/page.tsx           → wishlist feature
# Count: 2 distinct features → Goes in shared/
Multiple uses within the same feature count as 1, not multiple!
4

Create Migration Plan

Categorize components into three groups:1. Definitely Local (1 feature)
LoginForm          → Only in auth/login
ProductFilter      → Only in shop/products
CartSummary        → Only in shop/cart
2. Definitely Shared (2+ features)
Button             → Used everywhere
Modal              → Used in auth, shop, profile
ProductCard        → Used in shop, wishlist, recommendations
3. Uncertain (needs investigation)
UserAvatar         → Might be used in multiple features
ErrorBoundary      → Check actual usage

Phase 2: Create New Structure

1

Create Feature Directories

Set up your new structure without moving files yet:
# Create feature structure
mkdir -p src/app/features/{auth,shop,profile}
mkdir -p src/app/features/shared/{components,services,signals}

# Feature-specific subdirectories
mkdir -p src/app/features/auth/{login,register,components,services}
mkdir -p src/app/features/shop/{products,cart,components,services}
2

Move Definitely Local Components First

Start with components that are definitely used by only 1 feature:
# Example: Moving LoginForm (only used in auth/login)
git mv src/components/auth/LoginForm.tsx \
        src/app/features/auth/login/components/login-form.tsx
Update imports in the files that use it:
// Before
import { LoginForm } from '../../../components/auth/LoginForm';

// After (with path alias)
import { LoginForm } from '@features/auth/login/components/login-form';
3

Move Definitely Shared Components

Move components used by 2+ features to shared:
# Example: Moving Button (used everywhere)
git mv src/components/ui/Button.tsx \
        src/shared/components/ui/button.tsx
Update all imports:
// Before
import { Button } from '../../components/ui/Button';

// After
import { Button } from '@shared/components/ui/button';
4

Investigate Uncertain Components

For components where usage is unclear:
  1. Run search to find all imports
  2. Count distinct features
  3. Move based on the Scope Rule
  4. Add a comment documenting the decision
/**
 * UserAvatar - Shared Component
 * 
 * Used by:
 * - Profile feature (profile display)
 * - Auth feature (account settings)
 * - Dashboard feature (user info widget)
 * 
 * Decision: 3 features → Must be in shared/
 * Date: 2024-03-15
 */
export function UserAvatar({ user }: Props) {
  // Implementation
}

Phase 3: Modernize Framework Patterns

While migrating structure, also update to modern framework patterns.

Migrate to Angular 20 Patterns

1

Convert to Standalone Components

Before (NgModule):
// login.module.ts
import { NgModule } from '@angular/core';
import { LoginComponent } from './login.component';

@NgModule({
  declarations: [LoginComponent],
  imports: [CommonModule, ReactiveFormsModule],
})
export class LoginModule {}
After (Standalone):
// login.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-login',
  imports: [ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './login.html',
})
export class LoginComponent {
  // Component logic
}
2

Replace Lifecycle Hooks with Signals

Before (ngOnInit):
export class ProductsComponent implements OnInit {
  products: Product[] = [];
  loading = false;

  ngOnInit() {
    this.loading = true;
    this.productService.getProducts().subscribe(products => {
      this.products = products;
      this.loading = false;
    });
  }
}
After (Signals):
export class ProductsComponent {
  private readonly productService = inject(ProductService);
  
  readonly loading = signal(false);
  readonly products = signal<Product[]>([]);
  
  constructor() {
    this.loadProducts();
  }

  private async loadProducts() {
    this.loading.set(true);
    const data = await this.productService.getProducts();
    this.products.set(data);
    this.loading.set(false);
  }
}
3

Update to Modern Control Flow

**Before (ngIf, ngFor):
<div *ngIf="loading">Loading...</div>
<div *ngIf="!loading">
  <div *ngFor="let product of products">
    {{ product.name }}
  </div>
</div>
After (@if, @for):
@if (loading()) {
  <div>Loading...</div>
} @else {
  @for (product of products(); track product.id) {
    <div>{{ product.name }}</div>
  }
}

Complete Migration Example

Let’s walk through a full migration of an e-commerce app:

Before: Everything in One Place

src/
  components/
    auth/
      LoginForm.tsx
      RegisterForm.tsx
      SocialLogin.tsx
    shop/
      ProductCard.tsx
      ProductList.tsx
      ProductFilter.tsx
      AddToCart.tsx
    cart/
      CartItem.tsx
      CartSummary.tsx
    ui/
      Button.tsx
      Modal.tsx
      Input.tsx
  pages/
    login.tsx
    register.tsx
    shop.tsx
    cart.tsx

After: Scope Rule Applied (Next.js Example)

src/
  app/
    (auth)/
      login/
        page.tsx
        _components/
          login-form.tsx          # Only used in login
      register/
        page.tsx
        _components/
          register-form.tsx       # Only used in register
      _components/
        social-login.tsx          # Used by login + register (same feature)
      _actions/
        auth-actions.ts
    (shop)/
      shop/
        page.tsx
        _components/
          product-list.tsx        # Only used in shop page
          product-filter.tsx      # Only used in shop page
      cart/
        page.tsx
        _components/
          cart-item.tsx           # Only used in cart
          cart-summary.tsx        # Only used in cart
      _components/
        add-to-cart.tsx           # Used by shop + cart (same feature)
      _actions/
        product-actions.ts
        cart-actions.ts
  shared/                         # 2+ features only
    components/
      ui/
        button.tsx                # Used everywhere
        modal.tsx                 # Used in auth + shop
        input.tsx                 # Used everywhere
      product-card.tsx            # Used in shop + wishlist (if added)

What Changed and Why

Before: components/auth/LoginForm.tsxAfter: app/(auth)/login/_components/login-form.tsxWhy: Each form is only used in its specific route. Following co-location principle.
Before: components/auth/SocialLogin.tsxAfter: app/(auth)/_components/social-login.tsxWhy: Used by both login and register, but both are part of the same auth feature. Shared within the feature, not globally.
Initial: app/(shop)/_components/product-card.tsxIf used by wishlist feature later: Move to shared/components/product-card.tsxWhy: Currently only used in shop. If wishlist feature is added and uses it, then it becomes shared (2+ features).
Before: components/ui/Button.tsxAfter: shared/components/ui/button.tsxWhy: Button, Modal, Input are used across auth, shop, and potentially other features. Clear 2+ usage.

Migration Checklist

Use this checklist to track your migration progress:
  • Audit complete: All components inventoried with usage counts
  • Feature boundaries defined: Clear business features identified
  • New structure created: Directory structure set up
  • Path aliases configured: TypeScript/build tool configured
  • Local components moved: 1-feature components in place
  • Shared components moved: 2+ feature components in shared/
  • Imports updated: All import statements corrected
  • Framework patterns modernized: Using latest framework features
  • Tests passing: All tests updated and passing
  • Documentation updated: Team knows new structure
  • Old directories removed: Cleaned up old structure

Handling Team Migration

Don’t surprise your team with a massive restructure!

Communication Strategy

  1. Share the plan: Show the team the new structure before starting
  2. Migrate incrementally: One feature at a time, not everything at once
  3. Update documentation: Create a guide for the team
  4. Pair on first migrations: Work together on initial moves
  5. Set up linting: Add ESLint rules to enforce new structure

Example Team Guide

# Component Placement Guide

We're migrating to the Scope Rule architecture.

## Quick Decision Tree

1. Is this component used by only 1 feature?
   → Place in that feature's directory

2. Is this component used by 2+ features?
   → Place in `shared/components/`

## Examples

- LoginForm → Only in auth → `app/(auth)/login/_components/`
- Button → Used everywhere → `shared/components/ui/`
- ProductCard → Only in shop (for now) → `app/(shop)/_components/`

## Questions?

Ask in #architecture channel

Troubleshooting Common Issues

Problem: Components importing each other in circles.Solution:
  1. Identify the circular dependency
  2. Extract shared types to a separate file
  3. Use dependency injection or composition
  4. Consider if one component should be split
Problem: TypeScript can’t resolve @features/* imports.Solution:
  1. Verify tsconfig.json paths configuration
  2. Restart TypeScript server
  3. Check build tool config (Vite, Next.js, etc.)
  4. Ensure paths don’t conflict with node_modules
Problem: Tests can’t find moved components.Solution:
  1. Update test imports to use new paths
  2. Update mock paths in test setup
  3. Verify test path aliases match app config
  4. Run tests after each move to catch issues early
Problem: Not sure how many features use a component.Solution:
  1. Use IDE “Find All References”
  2. Use grep/ripgrep to search imports
  3. Add temporary console.log to see actual usage
  4. When uncertain, start local and move to shared if needed

Next Steps

Best Practices

Learn quality checks and patterns for maintaining clean architecture

Component Placement

Review the decision framework for placing new components

Use the Agents

Let the agents guide your architectural decisions

Examples

See real-world examples of the Scope Rule in action

Build docs developers (and LLMs) love