Project Structure β
Monorepo Organization β
The Campus project follows Nx monorepo conventions with clear separation between applications and shared libraries.
campus/
βββ apps/ # Applications (orchestration layer)
βββ libs/ # Shared libraries (decoupled modules)
βββ azure-pipelines/ # CI/CD configuration
βββ sprint_planning/ # Project planning filesArchitecture Evolution β
Current State (Legacy - Being Phased Out) β
- Libraries have direct dependencies on other libraries
- Complex dependency graph with tight coupling
- Shared services imported directly between libs
- State management scattered across multiple libs
Target State (Domain-Driven Design - Current Standard) β
- Complete domain isolation - each domain lib is entirely self-contained
- Domain-owned state management - each domain lib manages its own NgRx state (actions, reducers, effects, selectors)
- Domain-owned services - all services, interfaces, and injection tokens live within the domain
- Zero inter-domain dependencies - domains cannot import from other domains
- Apps as orchestration layer - apps wire up cross-domain communication through dependency injection
- Interface segregation - each domain defines minimal interfaces for external dependencies only
Domain-Driven Design (DDD) Principles β
Each domain library must be completely autonomous and include:
- State Management: Own NgRx store slice (actions, reducer, effects, selectors)
- Services: All business logic services within the domain
- Interfaces: Domain-specific interfaces and external dependency contracts
- Injection Tokens: All DI tokens for external dependencies
- Components/Directives: Domain-specific UI components
- Models/Types: All domain-related TypeScript interfaces and types
- Testing Utilities: Domain-specific mocks and test helpers
Migration Pattern β
When working on domain libs, follow the DDD approach:
- Self-contained domains: All domain logic, state, and services stay within the domain lib
- External dependency interfaces: Define minimal interfaces for what the domain needs from outside
- Injection tokens: Create tokens for all external dependencies
- App-level wiring: Let apps provide implementations for external dependencies
- No cross-domain imports: Never import directly from other domain libs
Applications (apps/) β
Main Applications β
polpo-classroom-web- Primary classroom management interface (default project)kabas-web- Kabas web applicationkabas-student- Student-facing Kabas applicationkabas-be- Kabas backend applicationpolpo-be- Polpo backend application
Specialized Tools β
ink/ink-polpo- Digital drawing applicationsspeech-editor/speech-kabas/speech-polpo- Audio editing toolstext-editor- Rich text editingtimeline-editor- Interactive timeline creationwhiteboard-standalone- Digital whiteboardcookie-consent- Cookie consent managementdocs-components- Component documentation
E2E Testing β
Each main application has a corresponding -e2e project for end-to-end testing.
Libraries (libs/) β
Core Infrastructure β
browser- Browser utilities and polyfillsenvironment- Environment configurationutils- Common utilitiestesting- Testing utilities and mocksdevlib- Development tools
UI & Components β
components- Shared UI componentsui- Additional UI utilitiessvg- SVG icon managementtheming- Theme system and stylinganimations- Animation utilities
Business Logic β
dal- Data Access Layershared- Shared business logicexercise- Exercise/assessment logicadaptive-learning- Adaptive learning algorithmssearch- Search functionality
Feature Libraries β
pages/- Page-specific components organized by featurecms/- Content management pagessettings/- Settings pagesstudents/- Student management- And many more...
External Integrations β
external/excel- Excel integrationexternal/froala- Froala editor integrationlearnosity- Learnosity assessment platformscorm- SCORM compliancecookies/cookies-consent- Cookie management
Specialized Features β
speech-editor/speech-player- Audio content toolstimeline- Timeline functionalitywhiteboard- Whiteboard featuresrich-text- Rich text editingloop- Learning loop functionalityab-test- A/B testing framework
Dependency Rules β
Current ESLint Rules (Legacy) β
- Libraries are tagged with
dep:prefixes indicating their dependency level - Higher-level libraries can depend on lower-level ones
- Applications can only depend on specific tagged libraries
- Core libraries (
browser,environment,utils) have minimal dependencies
Domain Dependency Pattern (DDD Standard) β
- Complete domain autonomy - each domain lib is entirely self-contained with its own state, services, and interfaces
- No cross-domain dependencies - domains cannot import from other domains
- Apps orchestrate cross-domain communication - only apps import and wire up multiple domains
- Token-based external dependencies - domains define interfaces for external needs, apps provide implementations
Domain-Driven Design Pattern β
Domain Structure β
Each domain lib follows this self-contained structure:
libs/[domain-name]/src/lib/
βββ store/ # NgRx state management
β βββ actions.ts # Domain actions
β βββ reducer.ts # Domain reducer
β βββ effects.ts # Domain effects
β βββ selectors.ts # Domain selectors
β βββ index.ts # Store exports
βββ services/ # Domain services
β βββ [domain].service.ts # Main domain service
β βββ *.service.ts # Supporting services
βββ interfaces/ # Domain interfaces
β βββ external/ # External dependency contracts
β β βββ auth.interface.ts
β β βββ api.interface.ts
β βββ models/ # Domain models
β βββ index.ts # Interface exports
βββ tokens/ # Injection tokens
β βββ external.tokens.ts # External dependency tokens
β βββ index.ts # Token exports
βββ components/ # Domain components
βββ directives/ # Domain directives
βββ testing/ # Domain test utilities
βββ index.ts # Public APIExternal Dependency Interface Definition β
Each domain defines minimal interfaces for external dependencies:
typescript
// libs/manage-tasks/src/lib/interfaces/external/auth.interface.ts
export interface TasksAuthService {
getCurrentUser(): Observable<User>;
}
// libs/manage-tasks/src/lib/tokens/external.tokens.ts
export const TASKS_AUTH_SERVICE = new InjectionToken<TasksAuthService>('TasksAuthService');Domain State Management β
Each domain manages its own NgRx state:
typescript
// libs/manage-tasks/src/lib/store/actions.ts
export const TaskActions = createActionGroup({
source: 'Tasks',
events: {
'Load Tasks': emptyProps(),
'Load Tasks Success': props<{ tasks: Task[] }>(),
'Load Tasks Failure': props<{ error: string }>(),
},
});
// libs/manage-tasks/src/lib/store/reducer.ts
interface TasksState {
tasks: Task[];
loading: boolean;
error: string | null;
}
export const tasksReducer = createReducer(/* ... */);
// libs/manage-tasks/src/lib/store/selectors.ts
export const selectTasksState = createFeatureSelector<TasksState>('tasks');
export const selectAllTasks = createSelector(selectTasksState, (state) => state.tasks);App-Level Domain Orchestration β
Apps wire up domains and provide external dependencies:
typescript
// apps/polpo-classroom-web/src/app/app.module.ts
@NgModule({
imports: [
// Domain modules
ManageTasksModule,
UserManagementModule,
// Domain state registration
StoreModule.forFeature('tasks', tasksReducer),
StoreModule.forFeature('users', usersReducer),
EffectsModule.forFeature([TasksEffects, UsersEffects]),
],
providers: [
// Shared services
AuthService,
ApiService,
// Domain external dependency wiring
{
provide: TASKS_AUTH_SERVICE,
useExisting: AuthService,
},
{
provide: TASKS_API_SERVICE,
useExisting: ApiService,
},
{
provide: USERS_AUTH_SERVICE,
useExisting: AuthService,
},
],
})
export class AppModule {}Domain Service Implementation β
Domains consume external dependencies through their defined interfaces:
typescript
// libs/manage-tasks/src/lib/services/tasks.service.ts
@Injectable()
export class TasksService {
constructor(
@Inject(TASKS_AUTH_SERVICE) private authService: TasksAuthService,
@Inject(TASKS_API_SERVICE) private apiService: TasksApiService,
private store: Store
) {}
loadTasks(): void {
this.store.dispatch(TaskActions.loadTasks());
}
}File Naming Conventions β
- Components:
kebab-casewithcampus-prefix - Directives:
camelCasewithcampusprefix - Files:
kebab-case.type.ts(e.g.,user.service.ts) - Test files:
*.spec.ts - E2E files:
*.e2e-spec.ts
NgRx Organization (Domain-Driven) β
Domain State Management β
- Domain-owned state: Each domain lib manages its own NgRx state slice
- Co-located store files: Actions, reducer, effects, and selectors live within the domain
- Feature state registration: Apps register domain state using
StoreModule.forFeature() - Domain effects: Each domain handles its own side effects and API calls
Naming Conventions β
- Actions: Use domain-prefixed event naming:
[DomainName] Event Description - Feature keys: Use domain name as feature key:
'tasks','users','ab-test' - Selectors: Prefix with domain:
selectTasksState,selectAllTasks - Effects: Domain-specific effects:
TasksEffects,UsersEffects
State Structure β
typescript
// App-level state shape
interface AppState {
tasks: TasksState; // Managed by tasks domain
users: UsersState; // Managed by users domain
abTest: ABTestState; // Managed by ab-test domain
}Cross-Domain Communication β
- No direct state access: Domains cannot access other domains' state
- App-level orchestration: Apps handle cross-domain communication
- Event-driven: Use domain events for loose coupling
- Service coordination: Apps coordinate between domain services when needed