Skip to content

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 configuration

Implementation 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 ​

  1. Identify domain boundaries - Group related functionality
  2. Extract domain state - Move NgRx state into domain libs
  3. Define external contracts - Create interfaces for external dependencies
  4. Create injection tokens - Replace direct imports with DI tokens
  5. Move services - Relocate services into appropriate domains
  6. Update apps - Wire up domains at app level
  7. 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)