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.

Apr 18, 2025 - 11:40
 0
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<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.