Organizing large-scale projects
Building large-scale applications with Aurelia 2 requires careful planning and organization. This guide provides best practices for structuring your projects, managing dependencies, and scaling your applications effectively.
Core Principles
Before diving into specific patterns, it's important to understand the core principles that guide these recommendations:
Separation of Concerns - Keep different aspects of your application isolated from each other
Scalability - Structure that grows with your team and application complexity
Maintainability - Code that's easy to understand, modify, and debug
Testability - Architecture that facilitates comprehensive testing
Performance - Structure that enables optimization without major refactoring
Project Structure Patterns
Feature-Based Architecture (Recommended)
Feature-based organization is recommended over technical-layer organization for several reasons:
Improved Cohesion: Related code stays together, making it easier to understand and modify features
Better Scalability: Teams can work on features independently without stepping on each other
Easier Code Splitting: Features naturally align with lazy-loading boundaries
Reduced Coupling: Features can be developed, tested, and deployed more independently
Here's how to structure a feature-based application:
Monorepo Structure
A monorepo structure is beneficial for large organizations because:
Code Sharing: Easy to share components, utilities, and types across applications
Atomic Changes: Can make coordinated changes across multiple packages in a single commit
Consistent Tooling: Single set of build tools, linting rules, and dependencies
Better Refactoring: IDE support for renaming and refactoring across all packages
Why Turbo?
Turbo is recommended for monorepo orchestration because:
Intelligent Caching: Only rebuilds what changed, dramatically speeding up builds
Parallel Execution: Runs tasks across packages in parallel when possible
Remote Caching: Teams can share build artifacts, reducing CI/CD times
Pipeline Management: Declaratively define task dependencies between packages
For enterprise applications with multiple teams or deployable units:
Application Bootstrap Patterns
Basic Bootstrap
Advanced Bootstrap with Configuration
State Management Architecture
State Management Options in Aurelia 2
Aurelia 2 provides two main approaches to state management:
DI-Based Services (Recommended for most cases)
Simple, testable, and TypeScript-friendly
No additional libraries or patterns to learn
Perfect for component-level and feature-level state
Works great with Aurelia's reactive binding system
@aurelia/state (For complex global state)
Redux-like state management with reactive bindings
Provides
@fromStatedecorator for component bindingsMemoized selectors for computed values
Action-based state updates with reducers
When to Use Each Approach
DI-Based Services
Use when:
State is scoped to a feature or component
You need simple CRUD operations
Testing is a priority
Team prefers familiar OOP patterns
You want minimal complexity
@aurelia/state
Use when:
You need truly global application state
Multiple unrelated components need the same state
You want predictable state updates through actions
Complex state relationships require memoized selectors
You need to debug state changes systematically
DI-Based State Management Pattern (Recommended)
Create dedicated state services using dependency injection:
Using @aurelia/state for Complex Scenarios
@aurelia/state provides Redux-like state management with reactive bindings:
Using DI-Based State in Components
Using @aurelia/state in Components
Template Usage with @aurelia/state
Comparison: When to Use Each
Learning Curve
Low
Medium
Boilerplate
Minimal
Medium
Computed Values
Manual
Memoized Selectors
State Scope
Feature/Component
Global
Testing
Excellent
Good
Performance
Good
Excellent
Best For
Most Use Cases
Complex Global State
Routing Patterns
Why Lazy Loading Routes?
Lazy loading routes is crucial for large applications because:
Faster Initial Load: Users only download code for the pages they visit
Better Caching: Browser can cache route bundles separately
Reduced Memory Usage: Components are only instantiated when needed
Natural Code Splitting: Each route becomes its own bundle
Static Route Configuration
Navigation Guards
Resource Management
Understanding Aurelia Resources
Resources in Aurelia 2 include:
Custom Elements: Reusable components
Custom Attributes: Behaviors attached to elements
Value Converters: Transform values in bindings
Binding Behaviors: Modify binding behavior
These need to be registered so Aurelia's template compiler can find them. You have two options:
Global Registration: Available everywhere in your app
Local Registration: Available only within a specific component or feature
Global Resource Registration
Use global registration for resources that are used frequently across your application:
Feature Module Pattern
Feature modules encapsulate all code for a specific domain. This pattern provides:
Clear Boundaries: Each feature is self-contained
Easy Testing: Can test features in isolation
Team Ownership: Teams can own entire features
Gradual Migration: Can migrate features incrementally
Build Configuration
Why Vite?
Vite is the recommended build tool for Aurelia 2 applications because:
Lightning Fast HMR: Near-instant hot module replacement during development
ESM-First: Native ES modules in development, optimized bundles for production
Zero Config: Works out of the box with sensible defaults
Built-in Optimizations: Automatic code splitting, tree shaking, and minification
First-Class TypeScript Support: No additional configuration needed
Modern Build with Vite
Environment Configuration
Why Environment-Specific Configuration?
Different environments require different settings:
Development: Verbose logging, local API endpoints, disabled analytics
Staging: Production-like but with test data and endpoints
Production: Optimized settings, real endpoints, enabled analytics
Using DI for environment configuration provides:
Type safety for configuration values
Easy mocking in tests
Single source of truth
Runtime configuration validation
Testing Strategies
Why @aurelia/testing?
Aurelia provides its own testing utilities because:
Lifecycle Management: Properly handles component lifecycle during tests
Fixture Creation: Easy setup of components with dependencies
DOM Assertions: Built-in helpers for testing rendered output
DI Integration: Seamlessly mock services through DI
Async Handling: Proper handling of Aurelia's async operations
Component Testing
Service Testing
Performance Optimization
Why Focus on Performance?
Large applications must consider performance from the start:
User Experience: Faster apps have better engagement and conversion
SEO Impact: Page speed affects search rankings
Mobile Users: Many users on slower connections or devices
Scalability: Performance problems compound as apps grow
Code Splitting with Dynamic Imports
Code splitting breaks your application into smaller chunks that load on demand:
Bundle Analysis
Micro-Frontend Architecture
When to Use Micro-Frontends?
Consider micro-frontends when:
Multiple Teams: Different teams own different parts of the application
Independent Deployment: Need to deploy features independently
Technology Diversity: Teams want to use different frameworks/versions
Massive Scale: Application is too large for a single codebase
Trade-offs
Pros:
Team autonomy
Independent deployments
Fault isolation
Technology flexibility
Cons:
Increased complexity
Potential duplication
Cross-module communication challenges
Larger overall bundle size
Shell Application
Remote Module
Decision Guide: Which Architecture to Choose?
Single Repository
Choose when:
Small to medium team (< 20 developers)
Single deployable application
Rapid prototyping needed
Simpler deployment pipeline preferred
Monorepo
Choose when:
Multiple related applications
Significant code sharing needed
Large team with good tooling
Consistent standards important
Micro-Frontends
Choose when:
Very large organization (100+ developers)
Teams need full autonomy
Independent deployment critical
Different tech stacks required
Best Practices Summary
Architecture Principles
Organize by features/domains, not technical layers
Use dependency injection for all services
Keep components focused on presentation
Implement proper separation of concerns
State Management
Use singleton services for application state
Implement request deduplication for concurrent calls
Handle loading and error states consistently
Keep state close to where it's used
Performance
Implement code splitting at route boundaries
Use dynamic imports for heavy dependencies
Monitor bundle sizes with analysis tools
Lazy load features when possible
Type Safety
Use TypeScript interfaces for all services
Avoid
anytype - create specific typesLeverage DI interfaces for better abstraction
Use strict TypeScript configuration
Testing
Test components with @aurelia/testing fixtures
Mock services through DI registration
Test state management logic separately
Focus on testing critical business logic and user workflows
Code Organization
Use barrel exports for feature modules
Keep consistent file naming conventions
Group related functionality together
Maintain clear module boundaries
Build & Deployment
Use modern build tools (Vite preferred)
Configure environment-specific settings
Implement proper code splitting
Monitor and optimize bundle sizes
By following these patterns and practices, you can build scalable, maintainable Aurelia 2 applications that grow with your team and business requirements. The key is to start with a solid foundation and evolve the architecture as needed while maintaining consistency across the codebase.
Last updated
Was this helpful?