Learnault API is built using Domain-Driven Design (DDD) principles with clear bounded contexts, a shared kernel, and event-driven communication between domains.
- Domain Boundaries: Each domain has clear ownership and responsibilities
- Loose Coupling: Domains communicate via events, not direct imports
- High Cohesion: Related functionality stays within domain boundaries
- Shared Kernel: Common infrastructure shared across all domains
- Infrastructure Separation: Infrastructure concerns separated from business logic
learnault-api/
├── src/
│ ├── domains/ # Business domains (bounded contexts)
│ │ ├── identity/ # Auth, registration, verification
│ │ ├── users/ # User profile management
│ │ ├── learning/ # Modules, completions, curriculum
│ │ ├── credentials/ # Digital credential issuance
│ │ ├── rewards/ # Reward calculation and distribution
│ │ ├── referrals/ # Referral tracking and bonuses
│ │ ├── notifications/ # Push notifications
│ │ ├── organizations/ # Employer/organization management
│ │ └── sync/ # Client-server synchronization
│ ├── shared/ # Shared kernel
│ │ ├── config/ # Configuration
│ │ ├── errors/ # Error handling
│ │ ├── middleware/ # Reusable middleware
│ │ ├── types/ # Common types
│ │ ├── utils/ # Utilities
│ │ └── messaging/ # Email, webhooks, events
│ ├── infrastructure/ # External integrations
│ │ └── blockchain/ # Stellar/Soroban integration
│ ├── app.ts # Express app setup
│ └── server.ts # Server entry point
├── docs/
│ ├── domains/ # Domain documentation
│ │ ├── DOMAIN_INVENTORY.md
│ │ ├── DOMAIN_DEFINITIONS.md
│ │ ├── SHARED_KERNEL.md
│ │ └── REQUEST_AND_EVENT_FLOWS.md
│ ├── ARCHITECTURE.md # This file
│ ├── API.md
│ ├── ERROR_HANDLING.md
│ └── SECURITY.md
├── integrations/
│ └── architecture/ # Architecture tests
│ └── domain-boundaries.test.ts
└── prisma/
└── schema.prisma # Database schema
-
Identity & Access (
domains/identity/)- User authentication and authorization
- JWT token management
- Email verification
-
User Management (
domains/users/)- User profiles and preferences
- Wallet address management
-
Learning Content (
domains/learning/)- Module/course management
- Learning progress tracking
- Completion recording
-
Credentials (
domains/credentials/)- Digital credential issuance
- On-chain credential storage
- Credential verification
-
Rewards (
domains/rewards/)- Reward calculation and distribution
- Balance management
- Withdrawal processing
-
Referrals (
domains/referrals/)- Referral code generation
- Referral tracking
- Bonus eligibility
-
Notifications (
domains/notifications/)- Push notification delivery
- Device token management
- Notification preferences
-
Organizations (
domains/organizations/)- Employer/organization management
- Organization verification
-
Synchronization (
domains/sync/)- Client-server sync
- Idempotency and conflict resolution
The shared kernel contains cross-cutting concerns accessible to all domains:
- Configuration (
shared/config/): Database, environment, logging - Error Handling (
shared/errors/): Error types and middleware - Middleware (
shared/middleware/): Auth, validation, rate limiting - Common Types (
shared/types/): API responses, pagination - Utilities (
shared/utils/): JWT, password hashing, helpers - Messaging (
shared/messaging/): Email/webhook delivery, event bus
Rules:
- All domains can import from shared kernel
- Shared kernel CANNOT import from domains
- Keep shared kernel minimal and stable
Infrastructure provides technical capabilities without business logic:
- Blockchain (
infrastructure/blockchain/): Stellar/Soroban integration - Database (
shared/config/database.ts): Prisma client
Rules:
- Infrastructure cannot import from business domains
- Domains call infrastructure via well-defined interfaces
- Infrastructure is replaceable (e.g., swap Stellar for different blockchain)
Domain → Shared Kernel ✅
Domain → Infrastructure ✅
Domain → Identity (for auth context) ✅
Domain A ← Domain B (via events only) ✅
Domain A → Domain B (direct import) ❌
Infrastructure → Domain ❌
Shared Kernel → Domain ❌
Circular dependencies ❌
Preferred:
- Domain Events (async, decoupled) - Use for cross-domain notifications
- Public Service Interfaces (sync, explicit) - Use sparingly for queries
- Database Queries (read-only) - Acceptable for simple lookups
Anti-Patterns:
- Direct controller-to-controller calls
- Direct service-to-service imports across domains
- Shared mutable state
Each domain follows this structure:
domains/[domain-name]/
├── controllers/ # HTTP request handlers
│ └── [domain].controller.ts
├── services/ # Business logic
│ └── [domain].service.ts
├── repositories/ # Data access (future)
│ └── [domain].repository.ts
├── types/ # Domain-specific types
│ └── [domain].types.ts
├── schemas/ # Validation schemas (Zod)
│ └── [domain].schema.ts
├── routes/ # Route definitions
│ └── [domain].routes.ts
├── events/ # Domain event definitions (future)
│ └── [domain].events.ts
├── handlers/ # Event handlers (future)
│ └── [domain].handlers.ts
└── index.ts # Barrel export
Client
↓ HTTP Request
Express App (app.ts)
↓ Middleware (auth, validation, rate-limit)
Domain Router
↓ Route handler
Domain Controller
↓ Input validation
Domain Service
↓ Business logic
├─→ Database (Prisma)
├─→ Infrastructure (Blockchain)
└─→ Messaging (Events, Email)
Domain Controller
↓ Response formatting
Client
↓ HTTP Response
Domain A
↓ Business logic executed
↓ Publish DomainEvent
Event Bus (future) / Direct Handler (current)
↓ Event dispatched
Domain B (Event Handler)
↓ React to event
↓ Execute business logic
↓ Publish new events (optional)
Database schema is defined in prisma/schema.prisma using Prisma ORM.
Key Models:
User- User accounts and authenticationModule- Learning contentCompletion- Module completion recordsCredential- Digital credentialsTransaction- Reward transactionsReferralCode,Referral- Referral trackingNotificationLog,DeviceToken- NotificationsEmailDelivery,WebhookDelivery- Outbox pattern
Each domain will have a repository layer to abstract database access:
// domains/users/repositories/user.repository.ts
export class UserRepository {
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } })
}
async create(data: CreateUserData): Promise<User> {
return prisma.user.create({ data })
}
}Benefits:
- Testable (mock repositories in tests)
- Encapsulates query logic
- Can switch database technology
Domain events represent something that has happened in the system:
interface DomainEvent {
eventId: string
eventType: string // e.g., "UserRegistered"
aggregateId: string // e.g., userId
aggregateType: string // e.g., "User"
payload: object
timestamp: Date
version: number
}Event bus will manage event publishing and subscription:
// shared/messaging/event-bus.ts
class EventBus {
publish(event: DomainEvent): void
subscribe(eventType: string, handler: EventHandler): void
}Each domain has event handlers that react to events from other domains:
// domains/rewards/handlers/module-completed.handler.ts
export class ModuleCompletedHandler {
async handle(event: ModuleCompletedEvent): Promise<void> {
// Calculate and distribute reward
}
}All errors extend AppError from shared kernel:
NotFoundError(404)ValidationError(400)UnauthorizedError(401)ForbiddenError(403)ConflictError(409)BadRequestError(400)
{
"error": "Resource not found",
"code": "NOT_FOUND",
"details": {
"resource": "User",
"id": "123"
}
}See docs/ERROR_HANDLING.md for details.
- Test business logic in isolation
- Mock external dependencies (database, blockchain, email)
- Located in
tests/unit/
- Test API endpoints end-to-end
- Use test database
- Located in
integrations/
- Enforce domain boundary rules
- Detect forbidden imports
- Detect circular dependencies
- Located in
integrations/architecture/
Run tests:
pnpm test # All tests
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage- JWT-based authentication
- Tokens signed with
JWT_SECRET - Token expiry configured via
JWT_EXPIRES_IN - Auth middleware:
shared/middleware/auth.middleware.ts
- Role-based access control (RBAC)
- Roles:
ADMIN,LEARNER,INSTRUCTOR - Checked in middleware and business logic
- API rate limiting: 100 requests per 15 minutes
- Auth rate limiting: 5 requests per 15 minutes
- Configured in
shared/middleware/rate-limit.middleware.ts
- Zod schemas for request validation
- Validation middleware:
shared/middleware/validation.middleware.ts
See docs/SECURITY.md for details.
API documentation is available at /api-docs when the server is running.
Configuration: src/config/swagger.ts
Current API version: v1
All routes are prefixed with /api/v1/
Future versions will be added as /api/v2/, maintaining backward compatibility.
See docs/API.md for endpoint details.
Required environment variables:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://...
JWT_SECRET=...
JWT_EXPIRES_IN=1d
STELLAR_NETWORK=testnet
STELLAR_SOURCE_SECRET=...
FIREBASE_SERVICE_ACCOUNT_KEY=...See .env.example for full list.
pnpm db:migrate # Run migrations
pnpm db:studio # Open Prisma Studio
pnpm seed # Seed databasedocker build -t learnault-api .
docker run -p 3000:3000 learnault-apiSee Dockerfile and docker-compose.yml.
Store all domain events for:
- Audit trail
- Event replay
- Temporal queries
Separate read and write models:
- Commands: Modify state
- Queries: Read-optimized views
Coordinate distributed transactions across domains:
- Orchestration-based sagas
- Compensating transactions for rollback
Centralized API gateway for:
- Rate limiting
- Authentication
- Routing
- Load balancing
src/
├── controllers/ # Flat controller structure
├── services/ # Flat service structure
├── routes/ # Flat routes
├── config/ # Mixed with business logic
└── ...
src/
├── domains/ # Bounded contexts
│ └── [domain]/
├── shared/ # Shared kernel
├── infrastructure/ # External integrations
└── ...
- ✅ Document domain boundaries
- ✅ Define shared kernel
- ⬜ Create domain folder structure
- ⬜ Move files to domains
- ⬜ Extract shared kernel
- ⬜ Implement event infrastructure
- ⬜ Refactor to event-driven
- ⬜ Add repository layer
- ⬜ Remove forbidden dependencies
- Domain Inventory
- Domain Definitions
- Shared Kernel
- Request and Event Flows
- API Documentation
- Error Handling
- Security
When adding new features:
- Identify which domain owns the feature
- Add business logic to domain service
- Add API endpoint to domain controller
- Update domain routes
- Publish domain events for cross-domain coordination
- Update architecture documentation
- Add architecture tests for new dependencies
See docs/CONTRIBUTING.md for details.
For questions about the architecture, please open an issue or discussion on GitHub.