Stack: Spring Boot 4 + WebFlux + Kotlin Coroutines + jOOQ + R2DBC + PostgreSQL
Layered architecture:
- Router (
*Router.kt) - Defines HTTP routes usingcoRouter { }DSL with error handlers - Handler (
*Handler.kt) - Suspended request handlers, delegates to service - Service (
*Service.kt) - Business logic,@Transactionalannotations, throwsNotFoundException - Repository (
*Repository.kt) - jOOQ DSL queries, returnsFlow<T>orsuspendfunctions
Key patterns:
- All HTTP handlers are
suspendfunctions using coroutines - Repositories use jOOQ's
asFlow()andawaitFirst/awaitFirstOrNullextensions - Service-layer
@Transactionalworks via R2DBC connection factory proxy - Make all external contracts openapi-generated, place them in contract subproject's resources, place client and model classes in the same package as the base package of the contract owner
- IMPORTANT: Always run
./gradlew clean teston a root project after any changes in contract subproject - Model mapping via extension functions (e.g.,
TestTable.toResponse()) - Don't try to replace something by default values on errors. If you received something unexpected, log and rethrow IllegalArgumentException on incoming and IllegalStateException on outgoing
- Never use optional Kotlin types for mandatory fields
- Always prefer imports to fully qualified class names
- Always remove unused imports and resources
- All versions in
build.gradle.ktsshould be variables from the rootbuild.gradle.kts - When adding new dependencies, ensure they are up to date
- Use jackson 3 for all components, avoid using jackson 2 when possible
- IMPORTANT: After changing
build.gradle.kts, always run./gradlew clean nativeCompile teston that subproject - IMPORTANT: After any changes, always run
./gradlew clean testin a corresponding subproject - Always consider fixing warning on build or application context startup during tests execution
- Please add comments to your code when the solution is not obvious, or you need to test several approaches
Database Patterns:
- Each subproject should use a separate database schema. It should be defined in subproject gradle.properties, application.yml (app and test) and @DBUnit annotation in AbstractContextTest
- Create migration file in
src/main/resources/db/migration/ - When creating migration, think about data migration when needed
- Run
./gradlew build- jOOQ classes regenerate automatically - Use generated classes in repository (e.g.,
TEST_TABLE,TestTablePOJO) - Don't use defaults in database schema, generation of id/uuid/timestamp should be explicit and code-based
- Never use optional Kotlin types for non-nullable fields. Please don't use nullability without a strong reason
Tests use Zonky embedded PostgreSQL with Flyway auto-migration.
- If the test requires a spring context and/or database, extend AbstractContextTest to avoid launching additional spring contexts and migrations
- In spring context tests never use class-wide annotations on test classes
- In spring context tests never use mocked beans in test classes to keep spring context clean
- Instead of mocks in a spring context test, add spies to AbstractContextTest to keep spring context clean
- For AbstractContextTest child tests use @DataSet instead of inserts for initial state (if the test is not covering inserts) and @ExpectedDataSet for checks if the test is not covering reads
- Use separate folders for repository and scenario datasets, datasets for each repository also should be in a separate folder
- Write tests for all application layers
- Never use sleep in tests
Tests are organized by layer:
*HandlerTest- WebTestClient-based HTTP endpoint tests*ServiceTest- Unit tests with MockK*RepositoryTest- Integration tests with an embedded database