This document describes the architectural decisions, structure, and principles adopted in this Angular project.
The goal of this architecture is to support:
- scalability
- maintainability
- testability
- accessibility
- predictable state and behavior
The structure reflects patterns commonly used in large-scale frontend applications.
The architecture is guided by the following principles:
Each layer has a clear responsibility, avoiding mixing UI, business logic, and infrastructure concerns.
Features are organized by business domain, not by technical type.
This improves discoverability and reduces cognitive load as the application grows.
Dependencies flow in a single direction:
- UI → Application Logic → Infrastructure
- Shared and Core layers never depend on Features
The architecture supports growth without introducing unnecessary abstraction upfront.
src/app
├── core
├── shared
├── features
├── state
Each folder represents a specific architectural layer.
Purpose:
Contains cross-cutting concerns that affect the entire application.
- Global error handling
- Logging and observability
- HTTP interceptors
- Application-wide services
- Environment-level abstractions
core/
├── error-handler/
│ └── global-error-handler.ts
├── interceptors/
│ └── http-error.interceptor.ts
├── logging/
│ └── logger.service.ts
- Must not depend on
features - Should be framework-aware but domain-agnostic
- Should be initialized once at application bootstrap
Purpose:
Holds reusable, domain-agnostic building blocks.
- UI components
- Pipes and directives
- Utility functions
- Design system abstractions
shared/
├── components/
│ ├── loading/
│ ├── alert/
│ └── bottom-sheet/
├── directives/
├── pipes/
├── utils/
- No business logic
- No direct API calls
- Must be reusable across multiple domains
Purpose:
Represents business domains and user journeys.
Each feature is self-contained and owns:
- its components
- its services
- its routing
- its business logic
features/
├── exchange/
│ ├── pages/
│ ├── components/
│ ├── services/
│ ├── exchange-routing.module.ts
│ └── exchange.module.ts
- Clear boundaries between features
- Lazy-loading friendly
- Easier to refactor or remove entire domains
- Features may depend on
sharedandstate - Features must not depend on other features directly
Purpose:
Centralize shared state using a reactive RxJS-based approach.
This layer avoids over-engineering (e.g. full NgRx) while still ensuring:
- predictable data flow
- explicit state ownership
- UI consistency
state/
├── user/
│ └── user-state.service.ts
├── exchange/
│ └── exchange-state.service.ts
BehaviorSubjectfor state storageObservablefor state consumption- Immutable state updates
For the scope of this project, a service-based reactive state:
- reduces boilerplate
- improves onboarding
- keeps complexity proportional
The architecture, however, allows future migration if needed.
Accessibility is considered at the architectural level, not only in components.
Key decisions:
- Semantic HTML over div-based layouts
- Accessible shared components
- Keyboard navigation built into base components
- Screen reader testing as part of development
Accessibility guidelines are detailed in accessibility.md.
The architecture supports testing by:
- isolating business logic in services
- avoiding logic inside templates
- keeping components thin
- enabling easy mocking of dependencies
This allows:
- fast unit tests
- stable E2E tests
- clearer test boundaries
This project follows modern Angular and RxJS testing practices.
BehaviorSubjectis used for state management, which emits values synchronously.- For this reason, tests avoid the use of
done()callbacks. - Instead, tests rely on:
- direct
subscribewhen synchronous, or firstValueFromwithasync/awaitfor clarity and type safety.
- direct
This approach:
- avoids TypeScript typing issues with
done - produces more deterministic tests
- aligns with current Angular and Jasmine best practices
This structure enables:
- adding new features without touching existing ones
- onboarding new developers faster
- parallel work across squads
- clearer ownership boundaries
The architecture intentionally mirrors patterns used in enterprise-grade Angular applications.
This architecture is designed to:
- scale with team size and complexity
- encourage best practices
- support accessibility, testing, and observability
- reflect real-world frontend engineering challenges
It prioritizes clarity over cleverness and engineering discipline over shortcuts.