DDD-like Kotlin object cooperating with Spring context
My friend Dominik helped me figure out which direction to take my coding skills. I never really liked the standard industry patterns that come with the Spring framework, where code is split into APIs, services, and repositories communicating via DTOs, DAOs, etc. I missed encapsulation and clear business objects. Plus, I enjoy both functional and OOP programming, and this layered approach often goes against those principles. DDD is full of abstract rules, but I decided to follow two simple ones: Domain objects are created on the edge of the domain layer or from another domain object. A domain object is a real object — it contains both data and business logic. Kotlin implementation Sticking to this leads to fluent, readable code that's easy to test and keeps domain problems well-encapsulated. Unfortunately, the Spring framework doesn’t really support this approach by default — it manages beans and stores them in the application context. So the question became: how can I keep clean, OOP-style domain objects while still calling functions that rely on the application context, like opening a DB transaction? The example below shows that if a function from a bean (usually a repository) is passed into a domain object as a lambda, the Spring context remains available — and even Spring aspects continue to work as expected. // GraphQl endpoint and edge of a domain layer @DgsComponent class DeleteCertificateMutation( private val certificateDbAdapter: CertificateDbAdapter, private val validator: DomainValidator, ) { @DgsMutation suspend fun deleteCertificate(id: String): Boolean = validator.existsCertificate(id) .deleteCertificate( certificateDbAdapter::deleteCertificate, ).getOrThrow() } // Snipped from Certificate domain object open suspend fun deleteCertificate( deleteCertificate: suspend (CertificateId) -> Unit, getSupplier: suspend (CertificateId) -> SupplierId, ): Either { // some business logic before like checking right etc. deleteCertificate(this@CertificateId).run { supplier.recalculateStatus() } true } Of course, there are also ways to avoid relying on the Spring context between the domain and repository layers, but in my case, I needed to use Spring Data R2DBC — probably the only reactive SQL database library that supports transactions — and it relies on aspects. Conclusion This approach allows you to have clean, OOP-style domain objects without losing the benefits of the Spring Framework environment. Domain objects are easy to unit test without needing mocking frameworks — which often lead to brittle tests focused too much on implementation details. To save time, I still use mock() for method or constructor arguments that aren't relevant to the test itself.

My friend Dominik helped me figure out which direction to take my coding skills. I never really liked the standard industry patterns that come with the Spring framework, where code is split into APIs, services, and repositories communicating via DTOs, DAOs, etc. I missed encapsulation and clear business objects. Plus, I enjoy both functional and OOP programming, and this layered approach often goes against those principles. DDD is full of abstract rules, but I decided to follow two simple ones:
- Domain objects are created on the edge of the domain layer or from another domain object.
- A domain object is a real object — it contains both data and business logic.
Kotlin implementation
Sticking to this leads to fluent, readable code that's easy to test and keeps domain problems well-encapsulated. Unfortunately, the Spring framework doesn’t really support this approach by default — it manages beans and stores them in the application context. So the question became: how can I keep clean, OOP-style domain objects while still calling functions that rely on the application context, like opening a DB transaction?
The example below shows that if a function from a bean (usually a repository) is passed into a domain object as a lambda, the Spring context remains available — and even Spring aspects continue to work as expected.
// GraphQl endpoint and edge of a domain layer
@DgsComponent
class DeleteCertificateMutation(
private val certificateDbAdapter: CertificateDbAdapter,
private val validator: DomainValidator,
) {
@DgsMutation
suspend fun deleteCertificate(id: String): Boolean =
validator.existsCertificate(id)
.deleteCertificate(
certificateDbAdapter::deleteCertificate,
).getOrThrow()
}
// Snipped from Certificate domain object
open suspend fun deleteCertificate(
deleteCertificate: suspend (CertificateId) -> Unit,
getSupplier: suspend (CertificateId) -> SupplierId,
): Either<NotAdminError, Boolean> {
// some business logic before like checking right etc.
deleteCertificate(this@CertificateId).run
{ supplier.recalculateStatus() }
true
}
Of course, there are also ways to avoid relying on the Spring context between the domain and repository layers, but in my case, I needed to use Spring Data R2DBC — probably the only reactive SQL database library that supports transactions — and it relies on aspects.
Conclusion
- This approach allows you to have clean, OOP-style domain objects without losing the benefits of the Spring Framework environment.
- Domain objects are easy to unit test without needing mocking frameworks — which often lead to brittle tests focused too much on implementation details.
- To save time, I still use mock() for method or constructor arguments that aren't relevant to the test itself.