Project Modules & Architecture
Overview
This document describes the project's module structure, dependency rules, and conventions for creating new modules.
Key Principle: Dependencies flow in one direction only. Circular and reverse dependencies are prohibited.
Architecture
This project follows a Layered Architecture.
graph TB
subgraph Bootstrap ["BOOTSTRAP"]
CommonAPI[common-api-app<br/>Controller → Facade]
API[skeleton-api-app]
Worker[skeleton-worker-app]
end
subgraph CommonWeb ["COMMON-WEB"]
Filters[Filters, Interceptors]
Handler[ExceptionHandler, ApiResource]
end
subgraph DomainLayer ["DOMAIN"]
App[Application<br/>Query / Command]
Svc[Service<br/>Business Logic]
Repo[Repository<br/>JPA + QueryDSL]
end
subgraph Infra ["INFRASTRUCTURE"]
Config[JPA Config, QueryDSL Config]
Cache[Cache, Redis, RestClient]
Export[Export, Slack]
end
subgraph Common ["COMMON"]
Utils[Codes, Exceptions, Utils, Extensions, Values]
end
CommonAPI --> CommonWeb
CommonAPI --> Infra
CommonAPI --> DomainLayer
API --> CommonWeb
API --> Infra
Worker --> CommonWeb
Worker --> Infra
CommonWeb --> Common
App --> Svc
Svc --> Repo
Infra --> DomainLayer
DomainLayer --> Common
Infra --> Common
Module structure
modules/ ├── common/ # Common utilities, exceptions, codes, value objects ├── common-web/ # Web common (Filter, Interceptor, Handler) ├── test-support/ # Test fixtures, REST Docs support ├── domain/ # Domain models, business rules, JPA Entity, Repository ├── infrastructure/ # Config, Cache, HTTP Client, external services ├── bootstrap/ # Spring Boot app modules │ ├── common-api-app/ # Common API server (Controller, Facade) │ ├── skeleton-api-app/ # API server │ └── skeleton-worker-app/ # Worker server └── docs/ # REST Docs document generation
Module dependencies
graph TD
CommonAPI[common-api-app] --> CW[common-web]
CommonAPI --> DOMAIN[domain]
CommonAPI --> INFRA[infrastructure]
API[skeleton-api-app] --> CW
API --> INFRA
API --> DOMAIN
Worker[skeleton-worker-app] --> CW
Worker --> INFRA
Worker --> DOMAIN
INFRA --> DOMAIN
INFRA --> COMMON[common]
DOMAIN --> COMMON
CW --> COMMON
style COMMON fill:#e1f5fe
style INFRA fill:#fff3e0
style CW fill:#f3e5f5
style DOMAIN fill:#e8f5e9
style CommonAPI fill:#ffebee
style API fill:#ffebee
style Worker fill:#ffebee
Dependency Direction Rule
IMPORTANT: All inter-module and inter-package dependencies must be unidirectional. Circular and reverse dependencies are prohibited.
- •
bootstrap → domain, infrastructure, common-web - •
infrastructure → domain, common - •
domain → common(only) - •
commonis the lowest-level module (depends on no other modules) - •
-appmodules serve as composition roots that assemble other modules - •Within domain:
dto → entity(DTO knows Entity). Entity must not import DTO
Modules
common
Common utility module. Foundation for all other modules.
| Package | Purpose |
|---|---|
codes/ | ResponseCode, ErrorCode, SuccessCode |
exceptions/ | BizException, BizRuntimeException, KnownException |
values/ | Email, PhoneNumber, Money, Rate value objects |
utils/datetime/ | DateFormatter, LocalDateRange, SearchDates |
utils/extensions/ | String masking, DateTime extensions |
utils/cipher/ | AES, SEED encryption |
utils/codec/ | URL encoding/decoding |
utils/coroutine/ | MDC-preserving coroutine utilities |
common-web
Web common infrastructure module.
| Package | Purpose |
|---|---|
filters/ | AppTraceFilter (UUID v7), ContentCachingFilter |
interceptors/ | LogInterceptor, LogResponseBodyInterceptor |
handlers/ | GlobalExceptionHandlerV2 |
response/ | ApiResource, Status, Meta, PageResponse |
aop/ | LogTraceAspect, CheckIpAspect |
domain
Domain models, business rules, JPA Entity, Repository. Depends only on the common module.
| Package | Purpose |
|---|---|
common/entity/ | BaseEntity, BaseTimeEntity (common audit entities) |
common/querydsl/ | QuerydslRepositorySupport, QuerydslExpressions (QueryDSL common) |
{feature}/dto/ | Domain DTOs (Info, Request, Exception) |
{feature}/entity/ | JPA Entity |
{feature}/repository/ | JpaRepository, QueryRepository |
{feature}/service/ | Business logic |
{feature}/application/ | QueryApplication (query), CommandApplication (create/update/delete) |
Request Flow
Controller (bootstrap) → Facade (bootstrap)
→ QueryApplication / CommandApplication (domain)
→ Service (domain)
→ JpaRepository / QueryRepository (domain)
infrastructure
Technical implementation module. Config, Cache, HTTP Client, external service integration.
| Package | Purpose |
|---|---|
client/ | RestClientConfig, HttpLoggingInterceptor |
cache/ | CacheConfig (Caffeine + Redis) |
redis/ | RedisLockAspect, RedisCacheAspect |
persistence/config/ | JpaConfig (Auditing), QuerydslConfig, DataSourceConfig (Master-Slave routing) |
export/ | Excel/CSV file export (annotation-based, style presets) |
slack/ | Slack SDK-based notifications (Kotlin DSL message builder) |
DataSource Routing (Master-Slave)
DataSourceConfig is activated by @ConditionalOnProperty(prefix = "spring.datasource.master.hikari", name = ["jdbc-url"]) and provides Master-Slave routing.
| Component | Role |
|---|---|
DataSourceConfig | Creates Master/Slave HikariCP pools, auto-activated when property exists |
RoutingDataSource | @Transactional(readOnly = true) routes to Slave, otherwise routes to Master |
LazyConnectionDataSourceProxy | Defers connection acquisition until actual query execution to ensure correct routing |
DataSource Configuration by Profile
| Profile | DB | DDL Auto | Master Pool | Slave Pool | Redisson |
|---|---|---|---|---|---|
embed, local | H2 In-Memory (single CP) | create-drop | - (max 10) | - | redisson-dev.yml |
dev, dev01, dev02 | MySQL Master-Slave | validate | max 10, idle 5 | max 10, idle 5 | redisson-dev.yml |
test, test01, test02 | MySQL Master-Slave | validate | max 10, idle 5 | max 10, idle 5 | redisson-test.yml |
stage, prod | MySQL Master-Slave | none | max 20, idle 5 | max 30, idle 10 | redisson-prod.yml |
- •
embed, local: Uses H2 auto-configuration (DataSourceConfigbean is not created) - •
dev, test, stage, prod:spring.datasource.master.hikari.jdbc-urlexists, activatingDataSourceConfig - •Aurora MySQL HikariCP settings:
max-lifetime: 840000(14min, for Aurora DNS failover TTL),connection-timeout: 3000(3s)
bootstrap
Spring Boot Application modules.
| Module | Purpose |
|---|---|
common-api-app | Common API server (Holiday API, data initialization) |
skeleton-api-app | REST API server (Controllers) |
skeleton-worker-app | Worker server (Scheduled Jobs) |
test-support
Test fixtures and REST Docs support module.
| Package | Purpose |
|---|---|
IntegratedTestSupport | Integration test base class |
EndPointTestSupport | API endpoint test base class |
RestDocsSupport | Spring REST Docs Kotlin DSL support |
TestTimeRunner | Test execution time measurement utility |
docs
API document generation module based on Spring REST Docs.
- •Generates HTML documents via the Asciidoctor plugin
- •Collects snippets generated from
common-api-appandskeleton-api-apptests - •Generate documents with
./gradlew :modules:docs:docs
Module naming convention
- •
-appsuffix: Spring Boot executable (bootJar enabled) - •No suffix: Library module (jar only)
Package naming convention
io.glory.domain.{feature}/
├── dto/
│ ├── {Feature}Info.kt # Domain DTO
│ ├── Create{Feature}Request.kt # Domain create request
│ └── {Feature}NotFoundException.kt # Domain exception
├── entity/
│ └── {Feature}.kt # JPA Entity
├── repository/
│ ├── {Feature}JpaRepository.kt # Spring Data JPA
│ └── {Feature}QueryRepository.kt # QueryDSL repository
├── service/
│ └── {Feature}Service.kt # Business logic
└── application/
├── {Feature}QueryApplication.kt # Query use cases
└── {Feature}CommandApplication.kt # Command use cases
io.glory.{appname}/
├── api/ # Controllers
├── dto/
│ ├── request/ # API request DTOs
│ └── response/ # API response DTOs
├── facade/ # Facade (DTO conversion + orchestration)
└── config/ # Configuration classes
Creating new module
Domain Feature
modules/domain/src/main/kotlin/io/glory/domain/myfeature/
├── dto/
│ ├── MyFeatureInfo.kt # Domain DTO
│ └── CreateMyFeatureRequest.kt # Domain request
├── entity/
│ └── MyFeature.kt # JPA Entity
├── repository/
│ ├── MyFeatureJpaRepository.kt # Spring Data JPA
│ └── MyFeatureQueryRepository.kt # QueryDSL repository
├── service/
│ └── MyFeatureService.kt # Business logic
└── application/
├── MyFeatureQueryApplication.kt # Query use cases
└── MyFeatureCommandApplication.kt # Command use cases
Bootstrap app module
modules/bootstrap/my-api-app/
├── build.gradle.kts
└── src/main/kotlin/io/glory/myapiapp/
├── api/
│ └── MyController.kt
├── dto/
│ ├── request/
│ │ └── MyApiRequest.kt
│ └── response/
│ └── MyApiResponse.kt
├── facade/
│ └── MyFacade.kt
├── config/
│ └── AppConfig.kt
└── MyApiApplication.kt
HTTP client pattern
Using @HttpExchange
@HttpExchange("/todos")
interface TodoClient {
@GetExchange
fun findAll(): List<TodoDto>
@GetExchange("/{id}")
fun findById(@PathVariable id: Int): TodoDto
@PostExchange
fun create(@RequestBody todo: TodoDto): TodoDto
}
Client configuration
@Configuration
@ImportHttpServices(group = "mygroup", types = [TodoClient::class])
class TodoClientConfig {
@Bean
fun todoClientGroupConfigurer(): RestClientHttpServiceGroupConfigurer {
return RestClientHttpServiceGroupConfigurer { groups ->
groups.filterByName("mygroup")
.forEachClient { _, builder ->
builder.baseUrl("https://api.example.com")
.requestInterceptor(HttpLoggingInterceptor("TodoClient"))
}
}
}
}
Response format
All API responses follow the ApiResource<T> format.
@GetMapping("/{id}")
fun findById(@PathVariable id: Int): ApiResource<Todo> {
return ApiResource.success(todoService.findById(id))
}
Response structure:
{
"status": {
"status": 200,
"code": "SUCCESS",
"message": "Operation successful"
},
"meta": {
"size": 1
},
"data": { ... }
}
Exception handling
Exception types
| Exception | Usage | Log Level |
|---|---|---|
KnownException | Expected errors (validation, not found) | INFO |
BizRuntimeException | Business errors (unrecoverable) | ERROR |
BizException | Checked business exceptions | ERROR |
Custom exception
class TodoNotFoundException(id: Int) : KnownException(
ErrorCode.DATA_NOT_FOUND,
"Todo not found: $id"
)
Caching strategy
Two-tier cache
| Layer | Configuration |
|---|---|
| L1 (Caffeine) | Local memory, 200 items, 30min TTL |
| L2 (Redis) | Distributed, configurable TTL |
Cache names
| Name | TTL | MaxIdle | Usage |
|---|---|---|---|
| SHORT_LIVED | 10min | 5min | Search, realtime data |
| DEFAULT | 30min | 10min | General queries |
| MID_LIVED | 1hour | 20min | Infrequently changed data |
| LONG_LIVED | 24hour | 4hour | Code tables, settings |