This document is authoritative.
All new code generated by AI tools must follow these rules. If a generated implementation conflicts with this document, the architecture defined here takes precedence.
This document defines the architectural conventions used in this project. All new code should follow these guidelines to ensure consistency, maintainability, and clean architecture principles.
The goal is to keep the codebase:
- Consistent
- Decoupled
- Testable
- Easy to reason about
This architecture follows ideas from Clean Architecture and Hexagonal Architecture, adapted pragmatically for this project.
The project is organized around features, not layers.
core/ -> cross-cutting infrastructure
features/ -> business functionality grouped by feature
Contains application-wide infrastructure:
core/
├── config
├── database
├── di
├── exceptions
├── plugins
└── security
Responsibilities:
- Application configuration
- Dependency injection
- Security (JWT, password hashing)
- Database initialization
- Framework plugins
Core must not depend on features.
All business logic lives inside features.
Example:
features/
├── auth
├── linking
├── semaphore
├── notification
└── user
Each feature is self-contained.
A typical feature structure:
feature_name/
├── dto/
├── ports/
├── FeatureRoutes.kt
├── FeatureServiceImpl.kt
├── Entity.kt
├── Repository.kt
└── Table.kt
File example:
LinkingRoutes.kt
AuthRoutes.kt
StatusRoutes.kt
Responsibilities:
- HTTP endpoints
- Request validation
- Authentication extraction
- Calling application services
- Returning HTTP responses
Routes must not contain business logic.
Services implement business logic and use cases.
Naming convention:
FeatureService (interface)
FeatureServiceImpl (implementation)
Example:
ports/LinkingService.kt
LinkingServiceImpl.kt
Services:
- Orchestrate business logic
- Call repositories
- Apply domain rules
- Throw domain exceptions
Services must not depend on framework details (Ktor, HTTP).
Ports define interfaces used for dependency inversion.
They live inside:
ports/
Types of ports:
Define the contract used by routes.
Example:
ports/LinkingService.kt
ports/AuthService.kt
Define persistence operations.
Example:
ports/AuthUserRepositoryPort.kt
ports/LinkSessionRepositoryPort.kt
Services depend on repository ports, not concrete implementations.
Repositories implement persistence logic.
Example:
LinkSessionRepository.kt
UserRepository.kt
SemaphoreRepository.kt
Responsibilities:
- Database access
- Mapping between DB rows and domain models
Repositories implement repository ports.
Example:
class LinkSessionRepository : LinkSessionRepositoryPort
Repositories should contain no business logic.
Domain models represent core entities.
Example:
LinkSession.kt
User.kt
Semaphore.kt
They contain:
- domain data
- domain states
- domain invariants (if simple)
They must not contain framework annotations unless strictly necessary.
Database schemas are defined using Exposed tables.
Example:
UserTable.kt
LinkSessionTable.kt
SemaphoreTable.kt
Tables should only define:
- column structure
- constraints
- indexes
No business logic.
DTOs represent data transferred through HTTP.
Located in:
dto/
Naming conventions:
CreateSomethingRequest
CreateSomethingResponse
SomethingStatusResponse
Example:
ConfirmLinkSessionRequest
CreateLinkSessionResponse
LinkSessionStatusResponse
DTOs must be serialization-friendly.
Example:
@Serializable
data class ConfirmLinkSessionRequest(
val linkCode: String
)DTO mapping should not live inside domain models.
Mapping functions may live in:
dto/DtoMappers.kt
Example:
fun toCreateResponse(...)
fun toStatusResponse(...)
This keeps:
- domain layer clean
- DTO logic separated
Dependency injection is handled using Koin.
Defined in:
core/di/KoinModules.kt
Example:
single<LinkingService> { LinkingServiceImpl(get()) }
single<LinkSessionRepositoryPort> { LinkSessionRepository() }
Always inject interfaces, not implementations.
Correct:
LinkingService -> LinkingServiceImpl
LinkSessionRepositoryPort -> LinkSessionRepository
Domain errors should use custom exceptions.
Example:
LinkSessionExpiredException
EmailAlreadyExistsException
Routes translate domain exceptions to HTTP responses.
Example:
409 Conflict
401 Unauthorized
410 Gone
Features should not directly depend on each other's implementations.
Communication between features should occur via:
- service ports
- domain events
- orchestrators
Example:
notification/NotificationOrchestrator.kt
FeatureService
FeatureServiceImpl
FeatureRepository
FeatureRepositoryPort
FeatureRoutes
ActionRequest
ActionResponse
SomethingStatusResponse
The project follows these principles:
- Single Responsibility Principle
- Dependency Inversion
- Feature modularity
- Explicit naming
- Small functions
- No hidden side effects
Avoid:
- fat routes
- business logic in repositories
- framework leakage into domain code
Use this structure:
features/newfeature/
├── dto/
│ ├── CreateThingRequest.kt
│ └── CreateThingResponse.kt
│
├── ports/
│ ├── ThingService.kt
│ └── ThingRepositoryPort.kt
│
├── ThingRoutes.kt
├── ThingServiceImpl.kt
├── ThingRepository.kt
├── Thing.kt
└── ThingTable.kt
This architecture aims to:
- keep the codebase consistent
- enable easier testing
- reduce coupling
- support future growth
- make the system understandable for humans and AI tools