-
Notifications
You must be signed in to change notification settings - Fork 3
Architecture Proposal: DDD Layered Architecture with Clear Separation of Concerns #2
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or requestquestionFurther information is requestedFurther information is requested
Description
Background
Currently, the src/ directory structure has some architectural inconsistencies:
- Naming confusion:
core/suggests domain core, but it actually contains infrastructure components (persistence, crypto, mail, jwt) - Missing layer: No application service layer - API routes directly instantiate domain services and infrastructure
- Leaky abstraction: API layer imports from both
domain/andcore/, bypassing proper encapsulation - Export complexity:
src/package.jsonexports many sub-paths, exposing internal implementation details
Example from services/account-api/src/routes/account.ts:
import { UserService } from 'edgeauth/domain/user';
import { D1UserRepository, hashPassword } from 'edgeauth/core';
import { PlunkSender, emailVerificationTemplate } from 'edgeauth/core/mail';This violates DDD principles where API should only depend on application services.
Proposed Architecture
Adopt standard DDD layered architecture with clear separation:
src/
├── infrastructure/ # Infrastructure Layer (renamed from core/)
│ ├── persistence/ # D1Repository, KVStore, etc.
│ ├── crypto/ # Password hashing, encryption
│ ├── mail/ # Email sending (Plunk, templates)
│ └── jwt/ # JWT generation and verification
│
├── domain/ # Domain Layer (keep as-is, with refinements)
│ ├── user/ # User aggregate + UserService (domain logic)
│ ├── oauth/ # OAuth aggregate + OAuthService
│ ├── sso/ # SSO aggregate + SSOService
│ └── verification/ # Verification logic
│
├── application/ # Application Layer (NEW - currently missing)
│ ├── AccountService.ts # User registration, verification, profile
│ ├── AuthService.ts # Login, token management
│ ├── OAuthService.ts # OAuth flows orchestration
│ └── ProfileService.ts # User profile management
│
└── index.ts # Public API - exports only application services
Layer Responsibilities
Infrastructure Layer (infrastructure/):
- Technical implementations: database access, crypto, email, JWT
- Implements interfaces defined by domain layer (dependency inversion)
- No business logic
Domain Layer (domain/):
- Pure domain logic: validation, business rules, domain object coordination
- Domain services for complex logic that doesn't belong to entities
- Defines repository interfaces (not implementations)
- No knowledge of infrastructure details
Application Layer (application/):
- Orchestrates complete business workflows
- Combines domain services + infrastructure
- Handles transactions, external integrations, logging
- Single entry point for API layer
Dependency Rules
API Layer (services/*)
↓ depends on
Application Layer (application/)
↓ depends on
Domain Layer (domain/) + Infrastructure Layer (infrastructure/)
↓
Domain defines interfaces, Infrastructure implements
Benefits
- Clear responsibilities: Each layer has well-defined purpose
- Better testability: Domain logic can be tested independently
- Simplified API: Services only import from
edgeauth(application services) - Dependency inversion: Domain defines contracts, infrastructure implements
- Easier maintenance: Changes in infrastructure don't affect domain logic
- Standard DDD: Follows established architectural patterns
Migration Steps
- Rename
core/toinfrastructure/ - Create
application/directory with service implementations - Update
src/index.tsto only export application services and necessary types - Simplify
src/package.jsonexports to only expose"." - Update all API services to import only from
edgeauthpackage - Update all import paths throughout the codebase
Example: Before vs After
Before (API directly using domain + infrastructure):
// services/account-api/src/routes/account.ts
import { UserService } from 'edgeauth/domain/user';
import { D1UserRepository, hashPassword } from 'edgeauth/core';
import { PlunkSender, emailVerificationTemplate } from 'edgeauth/core/mail';
account.post('/register', async (c) => {
const userRepository = new D1UserRepository(c.env.DB);
const userService = new UserService(userRepository);
const passwordHash = await hashPassword(body.password);
const user = await userService.register(data, passwordHash);
const mailSender = new PlunkSender(c.env.PLUNK_API_KEY, ...);
await mailSender.send({...});
// ... 40+ lines of orchestration logic
});After (API using application service):
// services/account-api/src/routes/account.ts
import { AccountService } from 'edgeauth';
account.post('/register', async (c) => {
const accountService = new AccountService({
db: c.env.DB,
jwtSecret: c.env.JWT_SECRET,
plunkApiKey: c.env.PLUNK_API_KEY,
emailFrom: c.env.EMAIL_FROM,
baseUrl: c.env.BASE_URL,
});
const result = await accountService.register({
email: body.email,
username: body.username,
password: body.password,
});
return c.json(result, 201);
});Questions for Discussion
- Should we introduce a factory pattern for service instantiation?
- How should we handle dependency injection for application services?
- Should
domain/services define their own repository interfaces, or use a shared repository pattern? - Timeline and migration strategy for existing services?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestquestionFurther information is requestedFurther information is requested