Domain-Driven Design Architecture β
Overview β
Campus follows a strict Domain-Driven Design (DDD) approach where each domain library is completely autonomous and self-contained. This ensures maximum decoupling, testability, and maintainability.
Core DDD Principles β
1. Complete Domain Autonomy β
Each domain library must be entirely self-contained with:
- Own state management (NgRx actions, reducer, effects, selectors)
- Own services (all business logic within the domain)
- Own interfaces (domain models and external dependency contracts)
- Own injection tokens (for external dependencies)
- Own components/directives (domain-specific UI)
- Own testing utilities (mocks and test helpers)
2. Zero Cross-Domain Dependencies β
- Domains CANNOT import from other domains
- Domains CANNOT access other domains' state directly
- Domains CANNOT use other domains' services directly
- All cross-domain communication happens at the app level
3. External Dependency Contracts β
Domains define minimal interfaces for external dependencies:
typescript
// libs/user-management/src/lib/interfaces/external/api.interface.ts
export interface UserApiService {
getUsers(): Observable<User[]>;
createUser(user: CreateUserRequest): Observable<User>;
}
// libs/user-management/src/lib/tokens/external.tokens.ts
export const USER_API_SERVICE = new InjectionToken<UserApiService>('UserApiService');Domain Structure Template β
Every domain library follows this structure:
libs/[domain-name]/
βββ src/lib/
β βββ store/ # NgRx State Management
β β βββ [domain].actions.ts # Domain actions
β β βββ [domain].reducer.ts # Domain reducer
β β βββ [domain].effects.ts # Domain effects
β β βββ [domain].selectors.ts # Domain selectors
β β βββ index.ts # Store exports
β βββ services/ # Domain Services
β β βββ [domain].service.ts # Main domain service
β β βββ *.service.ts # Supporting services
β βββ interfaces/ # Interfaces & Models
β β βββ external/ # External dependency contracts
β β β βββ *.interface.ts # External service interfaces
β β β βββ index.ts # External interface exports
β β βββ models/ # Domain models
β β β βββ *.model.ts # Domain entities
β β β βββ index.ts # Model exports
β β βββ index.ts # All interface exports
β βββ tokens/ # Injection Tokens
β β βββ external.tokens.ts # External dependency tokens
β β βββ internal.tokens.ts # Internal service tokens (if needed)
β β βββ index.ts # Token exports
β βββ components/ # Domain Components
β β βββ [component]/ # Component folders
β β βββ index.ts # Component exports
β βββ directives/ # Domain Directives
β β βββ *.directive.ts # Domain directives
β β βββ index.ts # Directive exports
β βββ testing/ # Testing Utilities
β β βββ mocks/ # Mock services
β β βββ fixtures/ # Test data
β β βββ utilities/ # Test helpers
β β βββ index.ts # Testing exports
β βββ [domain].module.ts # Domain module
β βββ index.ts # Public API
βββ README.md # Domain documentation
βββ project.json # Nx project configurationImplementation Guidelines β
1. State Management β
Each domain owns its NgRx state:
typescript
// libs/ab-test/src/lib/store/ab-test.actions.ts
export const ABTestActions = createActionGroup({
source: 'AB Test',
events: {
'Load Variant Assignment': props<{ testId: string }>(),
'Load Variant Assignment Success': props<{ assignment: VariantAssignment }>(),
'Track Event': props<{ event: ABTestEvent }>(),
},
});
// libs/ab-test/src/lib/store/ab-test.reducer.ts
interface ABTestState {
assignments: Record<string, VariantAssignment>;
loading: boolean;
error: string | null;
}
export const abTestReducer = createReducer(/* ... */);
// libs/ab-test/src/lib/store/ab-test.selectors.ts
export const selectABTestState = createFeatureSelector<ABTestState>('abTest');
export const selectVariantAssignment = (testId: string) =>
createSelector(selectABTestState, (state) => state.assignments[testId]);2. Service Implementation β
Domain services use dependency injection for external dependencies:
typescript
// libs/ab-test/src/lib/services/ab-test.service.ts
@Injectable()
export class ABTestService {
constructor(
@Inject(AB_TEST_API_SERVICE) private apiService: ABTestApiService,
@Inject(AB_TEST_AUTH_SERVICE) private authService: ABTestAuthService,
private store: Store
) {}
getVariantAssignment(testId: string): Observable<VariantType> {
return this.store.select(selectVariantAssignment(testId)).pipe(
tap((assignment) => {
if (!assignment) {
this.store.dispatch(ABTestActions.loadVariantAssignment({ testId }));
}
}),
map((assignment) => assignment?.variantId)
);
}
}3. External Dependency Interfaces β
Define minimal contracts for what the domain needs:
typescript
// libs/ab-test/src/lib/interfaces/external/auth.interface.ts
export interface ABTestAuthService {
getCurrentUserId(): Observable<string>;
}
// libs/ab-test/src/lib/interfaces/external/api.interface.ts
export interface ABTestApiService {
getVariantAssignment(testId: string, userId: string): Observable<VariantAssignment>;
trackEvent(event: ABTestEvent): Observable<void>;
}
// libs/ab-test/src/lib/tokens/external.tokens.ts
export const AB_TEST_AUTH_SERVICE = new InjectionToken<ABTestAuthService>('ABTestAuthService');
export const AB_TEST_API_SERVICE = new InjectionToken<ABTestApiService>('ABTestApiService');4. App-Level Orchestration β
Apps wire up domains and provide external dependencies:
typescript
// apps/polpo-classroom-web/src/app/app.module.ts
@NgModule({
imports: [
// Domain modules
ABTestModule,
UserManagementModule,
// Domain state registration
StoreModule.forFeature('abTest', abTestReducer),
StoreModule.forFeature('users', usersReducer),
EffectsModule.forFeature([ABTestEffects, UsersEffects]),
],
providers: [
// Shared services (app-level)
AuthService,
ApiService,
// AB Test domain dependencies
{
provide: AB_TEST_AUTH_SERVICE,
useExisting: AuthService,
},
{
provide: AB_TEST_API_SERVICE,
useExisting: ApiService,
},
// User Management domain dependencies
{
provide: USER_AUTH_SERVICE,
useExisting: AuthService,
},
{
provide: USER_API_SERVICE,
useExisting: ApiService,
},
],
})
export class AppModule {}Testing Strategy β
Domain Testing β
Each domain includes comprehensive testing utilities:
typescript
// libs/ab-test/src/lib/testing/mocks/ab-test-api.mock.ts
export class MockABTestApiService implements ABTestApiService {
getVariantAssignment = jest.fn();
trackEvent = jest.fn();
}
// libs/ab-test/src/lib/testing/utilities/ab-test.utilities.ts
export class ABTestTestingUtils {
static forceVariant(testId: string, variantId: VariantType): void {
// Test utility implementation
}
static createMockEvent(overrides?: Partial<ABTestEvent>): ABTestEvent {
// Mock event creation
}
}Integration Testing β
Apps test domain integration:
typescript
// apps/polpo-classroom-web/src/app/app.component.spec.ts
describe('App Integration', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ABTestModule, UserManagementModule],
providers: [
{ provide: AB_TEST_AUTH_SERVICE, useClass: MockAuthService },
{ provide: AB_TEST_API_SERVICE, useClass: MockApiService },
],
});
});
it('should wire up domain dependencies correctly', () => {
// Integration test
});
});Migration Guidelines β
From Legacy to DDD β
- Identify domain boundaries - Group related functionality
- Extract domain state - Move NgRx state into domain libs
- Define external contracts - Create interfaces for external dependencies
- Create injection tokens - Replace direct imports with DI tokens
- Move services - Relocate services into appropriate domains
- Update apps - Wire up domains at app level
- Remove cross-domain imports - Eliminate direct lib-to-lib dependencies
Validation Checklist β
For each domain library, verify:
- [ ] No imports from other domain libs
- [ ] Own NgRx state management (actions, reducer, effects, selectors)
- [ ] All services within the domain
- [ ] External dependency interfaces defined
- [ ] Injection tokens for external dependencies
- [ ] Testing utilities included
- [ ] Public API clearly defined
- [ ] Documentation updated
Benefits β
Maintainability β
- Clear boundaries and responsibilities
- Easier to understand and modify
- Reduced cognitive load
Testability β
- Isolated testing of domains
- Easy mocking of external dependencies
- Comprehensive test coverage
Scalability β
- Independent development of domains
- Parallel team work
- Easier onboarding
Flexibility β
- Easy to swap implementations
- Domain-specific optimizations
- Independent deployment (future)