Skip to main content
Version: 0.1.0 (Latest)

Spring Boot Integration

This guide shows you how to integrate Structus with Spring Boot.

Overview

Spring Boot is a popular framework for building Java and Kotlin applications. Structus can be seamlessly integrated with Spring Boot to provide a clean architecture foundation for your application.

Setup

Dependencies

Add the following to your build.gradle.kts:

dependencies {
    // Structus
    implementation("com.github.melsardes:structus-kotlin:0.1.0")
    
    // Spring Boot
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    
    // Kotlin Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
}

Coroutines Configuration

Configure Spring to work with Kotlin coroutines:

@Configuration
class CoroutineConfig {
    @Bean
    fun coroutineDispatcher(): CoroutineDispatcher = Dispatchers.Default
    
    @Bean
    fun coroutineScope(coroutineDispatcher: CoroutineDispatcher): CoroutineScope =
        CoroutineScope(coroutineDispatcher + SupervisorJob())
}

Domain Layer

Create your domain entities and repositories:

data class ProductId(val value: String) : ValueObject

class Product private constructor(
    override val id: ProductId,
    private var name: String,
    private var price: Money,
    private var active: Boolean = true
) : AggregateRoot<ProductId>() {
    // Properties and methods
    
    companion object {
        fun create(id: ProductId, name: String, price: Money): Product {
            // Create product and record events
            val product = Product(id, name, price)
            product.recordEvent(ProductCreatedEvent(id.value, name, price.amount))
            return product
        }
    }
}

interface ProductRepository : Repository {
    suspend fun findById(id: ProductId): Product?
    suspend fun save(product: Product)
}

Application Layer

Create command and query handlers:

data class CreateProductCommand(val name: String, val price: BigDecimal) : Command

@Component
class CreateProductCommandHandler(
    private val productRepository: ProductRepository,
    private val outboxRepository: MessageOutboxRepository
) : CommandHandler<CreateProductCommand, Result<ProductId>> {
    
    override suspend operator fun invoke(command: CreateProductCommand): Result<ProductId> {
        return runCatching {
            val id = ProductId(UUID.randomUUID().toString())
            val price = Money(command.price, Currency.getInstance("USD"))
            
            val product = Product.create(id, command.name, price)
            productRepository.save(product)
            
            // Save events to outbox
            product.domainEvents.forEach { outboxRepository.save(it) }
            product.clearEvents()
            
            id
        }
    }
}

Infrastructure Layer

Implement the repositories:

@Repository
class ProductRepositoryImpl(private val jdbcTemplate: JdbcTemplate) : ProductRepository {
    
    override suspend fun findById(id: ProductId): Product? = withContext(Dispatchers.IO) {
        try {
            jdbcTemplate.queryForObject(
                "SELECT id, name, price, currency, active FROM products WHERE id = ?",
                { rs, _ -> mapToProduct(rs) },
                id.value
            )
        } catch (e: EmptyResultDataAccessException) {
            null
        }
    }
    
    override suspend fun save(product: Product) = withContext(Dispatchers.IO) {
        // Implementation details
    }
    
    private fun mapToProduct(rs: ResultSet): Product {
        // Mapping logic
    }
}

Presentation Layer

Create REST controllers:

@RestController
@RequestMapping("/api/v1/products")
class ProductController(
    private val createProductHandler: CreateProductCommandHandler,
    private val getProductHandler: GetProductQueryHandler
) {
    
    @PostMapping
    suspend fun createProduct(@RequestBody request: CreateProductRequestDto): ResponseEntity<Any> {
        val command = CreateProductCommand(name = request.name, price = request.price)
        
        return when (val result = createProductHandler(command)) {
            is Result.Success -> ResponseEntity
                .status(HttpStatus.CREATED)
                .body(mapOf("id" to result.value.value))
            is Result.Failure -> ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(mapOf("error" to (result.error.message ?: "Unknown error")))
        }
    }
    
    @GetMapping("/{id}")
    suspend fun getProduct(@PathVariable id: String): ResponseEntity<Any> {
        // Implementation
    }
}

Event Publishing

Set up a scheduled task to publish events from the outbox:

@Component
class OutboxPublisher(
    private val outboxRepository: MessageOutboxRepository,
    private val eventPublisher: DomainEventPublisher
) {
    
    @Scheduled(fixedDelay = 1000)
    fun publishPendingEvents() = runBlocking {
        val messages = outboxRepository.findUnpublished(100)
        
        messages.forEach { message ->
            try {
                eventPublisher.publish(message.event)
                outboxRepository.markAsPublished(message.id)
            } catch (e: Exception) {
                // Error handling
            }
        }
    }
}

Database Schema

CREATE TABLE products (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(19, 4) NOT NULL,
    currency VARCHAR(3) NOT NULL,
    active BOOLEAN NOT NULL
);

CREATE TABLE message_outbox (
    id VARCHAR(36) PRIMARY KEY,
    event_type VARCHAR(255) NOT NULL,
    aggregate_id VARCHAR(255) NOT NULL,
    event_data TEXT NOT NULL,
    published BOOLEAN NOT NULL DEFAULT false,
    timestamp TIMESTAMP NOT NULL
);

Testing

Create integration tests:

@SpringBootTest
class ProductIntegrationTest {
    
    @Autowired
    private lateinit var productController: ProductController
    
    @Test
    fun `should create and retrieve a product`() = runBlocking {
        // Test implementation
    }
}

Example Project

Check out our complete example project on GitHub to see a full Spring Boot integration with Structus.

Next Steps

Explore the Ktor Integration guide to see how to use Structus with Ktor.