Skip to content

Latest commit

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Structus Logo

Structus - Kotlin Architecture Toolkit

KotlinLicenseVersionAI Agent Friendly

Structus Banner

A pure Kotlin JVM library providing the foundational building blocks for implementing Explicit Architecture—a synthesis of Domain-Driven Design (DDD), Command/Query Separation (CQS), and Event-Driven Architecture (EDA).

🎯 Purpose

This library serves as a shared kernel for large-scale projects, defining the interfaces and base classes for all core business concepts and architectural patterns. It enforces clean architecture principles while remaining completely framework-agnostic.

✨ Key Features

  • 🚀 Pure Kotlin: No framework dependencies (Spring, Ktor, Micronaut, etc.)
  • 🔄 Coroutine-Ready: All I/O operations use suspend functions
  • 📦 Minimal Dependencies: Only Kotlin stdlib + kotlinx-coroutines-core
  • 📚 Comprehensive Documentation: Every component includes KDoc and examples
  • 🏗️ Framework-Agnostic: Works with any framework or pure Kotlin
  • 🎨 Clean Architecture: Enforces proper layer separation and dependencies

📦 Installation

Building from Source

Since the library is not yet published to a public repository, you'll need to build and install it locally:

1. Clone and Build

git clone https://github.com/structus-io/structus-kotlin.git
cd structus-kotlin
./gradlew build publishToMavenLocal

This will install the library to your local Maven repository (~/.m2/repository).

2. Add to Your Project

Gradle (Kotlin DSL)

repositories {
mavenLocal() // Add local Maven repository
mavenCentral()
}
dependencies {
implementation("com.melsardes.libraries:structus-kotlin:0.1.0")
}

Gradle (Groovy)

repositories {
mavenLocal() // Add local Maven repository
mavenCentral()
}
dependencies {
implementation 'com.melsardes.libraries:structus-kotlin:0.1.0'
}

Maven

<dependency>
<groupId>com.melsardes.libraries</groupId>
<artifactId>structus-kotlin</artifactId>
<version>0.1.0</version>
</dependency>

Note: Maven automatically checks the local repository (~/.m2/repository) before remote repositories.

🏛️ Architecture Components

Domain Layer (com.melsardes.libraries.structuskotlin.domain)

Core Building Blocks

// Entity: Identity-based domain objectsabstractclassEntity<ID> {
abstractval id:ID// equals/hashCode based on ID
}
// Value Object: Attribute-based immutable objectsinterfaceValueObject// Aggregate Root: Consistency boundary with event management and lifecycleabstractclassAggregateRoot<ID> : Entity<ID>() {
val domainEvents:List<DomainEvent>
protectedfunrecordEvent(event:DomainEvent)
funclearEvents()
// Lifecycle managementinternalfunmarkAsCreated(by:String, at: kotlin.time.Instant = Clock.System.now())
internalfunmarkAsUpdated(by:String, at: kotlin.time.Instant = Clock.System.now())
funsoftDelete(by:String, at: kotlin.time.Instant = Clock.System.now())
funrestore(by:String, at: kotlin.time.Instant = Clock.System.now())
funisDeleted(): BooleanfunisActive(): BooleaninternalfunincrementVersion()
}
// Repository: Persistence contractinterfaceRepository

Events

// Domain Event: Something that happenedinterfaceDomainEvent {
val eventId:Stringval occurredAt: kotlin.time.Instant// Uses Kotlin multiplatform time APIval aggregateId:String
}
// Transactional Outbox PatterninterfaceMessageOutboxRepository : Repository {
suspendfunsave(event:DomainEvent)
suspendfunfindUnpublished(limit:Int): List<OutboxMessage>
suspendfunmarkAsPublished(messageId:String)
suspendfunincrementRetryCount(messageId:String)
}

Application Layer - Commands (com.melsardes.libraries.structuskotlin.application.commands)

// Command: Intent to change stateinterfaceCommand// Command Handler: Executes business logic (uses invoke operator)interfaceCommandHandler<inC:Command, outR> {
suspendoperatorfuninvoke(command:C): R
}
// Command Bus: Dispatches commands to handlersinterfaceCommandBus {
fun <C:Command, R> register(commandClass:KClass<C>, handler:CommandHandler<C, R>)
suspendfun <C:Command, R> dispatch(command:C): R
}

Application Layer - Queries (com.melsardes.libraries.structuskotlin.application.queries)

// Query: Request for datainterfaceQuery// Query Handler: Retrieves data (uses invoke operator)interfaceQueryHandler<inQ:Query, outR> {
suspendoperatorfuninvoke(query:Q): R
}

Application Layer - Events (com.melsardes.libraries.structuskotlin.application.events)

// Domain Event Publisher: Publishes events to external systemsinterfaceDomainEventPublisher {
suspendfunpublish(event:DomainEvent)
suspendfunpublishBatch(events:List<DomainEvent>)
}

🚀 Quick Start

1. Define Your Domain Model

// Value Objectdata classEmail(valvalue:String) : ValueObject {
init {
require(value.matches(EMAIL_REGEX)) { "Invalid email format" }
}
companionobject {
privatevalEMAIL_REGEX="^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".toRegex()
}
}
// Entity IDdata classUserId(valvalue:String) : ValueObject
// Aggregate RootclassUser(
overridevalid:UserId,
varemail:Email,
varname:String,
varstatus:UserStatus
) : AggregateRoot<UserId>() {
funregister(email:Email, name:String) {
this.email = email
this.name = name
this.status =UserStatus.ACTIVE
recordEvent(UserRegisteredEvent(
aggregateId = id.value,
userId = id.value,
email = email.value,
registeredAt = kotlin.time.Clock.System.now()
))
}
companionobject {
funcreate(email:Email, name:String): User {
val user =User(
id =UserId(UUID.randomUUID().toString()),
email = email,
name = name,
status =UserStatus.PENDING
)
user.register(email, name)
return user
}
}
}
// Domain Eventdata classUserRegisteredEvent(
overridevaleventId:String = UUID.randomUUID().toString(),
overridevaloccurredAt: kotlin.time.Instant = kotlin.time.Clock.System.now(),
overridevalaggregateId:String,
valuserId:String,
valemail:String,
valregisteredAt: kotlin.time.Instant
) : DomainEvent
// Repository InterfaceinterfaceUserRepository : Repository {
suspendfunfindById(id:UserId): User?suspendfunfindByEmail(email:Email): User?suspendfunsave(user:User)
suspendfunexistsByEmail(email:Email): Boolean
}

2. Define Commands and Handlers

// Commanddata classRegisterUserCommand(
valemail:String,
valname:String
) : Command {
init {
require(email.isNotBlank()) { "Email cannot be blank" }
require(name.isNotBlank()) { "Name cannot be blank" }
}
}
// Command HandlerclassRegisterUserCommandHandler(
privatevaluserRepository:UserRepository,
privatevaloutboxRepository:MessageOutboxRepository
) : CommandHandler<RegisterUserCommand, Result<UserId>> {
overridesuspendoperatorfuninvoke(command:RegisterUserCommand): Result<UserId> {
return runCatching {
// Check if email already existsif (userRepository.existsByEmail(Email(command.email))) {
throwIllegalStateException("Email already exists")
}
// Create userval user =User.create(
email =Email(command.email),
name = command.name
)
// Save user
userRepository.save(user)
// Save events to outbox (Transactional Outbox Pattern)
user.domainEvents.forEach { event ->
outboxRepository.save(event)
}
// Clear events
user.clearEvents()
user.id
}
}
}

3. Define Queries and Handlers

// Querydata classGetUserByIdQuery(
valuserId:String
) : Query
// DTOdata classUserDto(
valid:String,
valemail:String,
valname:String,
valstatus:String
)
// Query HandlerclassGetUserByIdQueryHandler(
privatevaluserRepository:UserRepository
) : QueryHandler<GetUserByIdQuery, UserDto?> {
overridesuspendoperatorfuninvoke(query:GetUserByIdQuery): UserDto? {
val user = userRepository.findById(UserId(query.userId))
return user?.let {
UserDto(
id = it.id.value,
email = it.email.value,
name = it.name,
status = it.status.name
)
}
}
}

4. Use in Your Application

// In your controller/endpointclassUserController(
privatevalcommandBus:CommandBus,
privatevalgetUserByIdHandler:GetUserByIdQueryHandler
) {
suspendfunregisterUser(request:RegisterUserRequest): UserResponse {
val command =RegisterUserCommand(
email = request.email,
name = request.name
)
val result = commandBus.dispatch(command)
return result.fold(
onSuccess = { userId ->UserResponse(userId = userId.value) },
onFailure = { throw it }
)
}
suspendfungetUser(userId:String): UserDto? {
val query =GetUserByIdQuery(userId)
return getUserByIdHandler(query) // Invoke operator
}
}

📖 Documentation

🏗️ Project Structure

lib/src/main/kotlin/com/melsardes/libraries/structuskotlin/
├── domain/
│ ├── Entity.kt # Base entity class
│ ├── ValueObject.kt # Value object marker
│ ├── AggregateRoot.kt # Aggregate root with events & lifecycle
│ ├── Repository.kt # Repository marker
│ ├── MessageOutboxRepository.kt # Outbox pattern support
│ ├── Result.kt # Result type for error handling
│ └── events/
│ ├── DomainEvent.kt # Domain event interface
│ └── BaseDomainEvent.kt # Base event implementation
├── application/
│ ├── commands/
│ │ ├── Command.kt # Command marker
│ │ ├── CommandHandler.kt # Command handler (invoke operator)
│ │ └── CommandBus.kt # Command bus interface
│ ├── queries/
│ │ ├── Query.kt # Query marker
│ │ └── QueryHandler.kt # Query handler (invoke operator)
│ └── events/
│ ├── DomainEventPublisher.kt # Event publisher interface
│ └── DomainEventHandler.kt # Event handler interface

🎯 Design Principles

1. Dependency Rule

Layers can only depend on layers beneath them:

  • Domain → Nothing (pure business logic)
  • Application → Domain
  • Infrastructure → Domain + Application
  • Presentation → Application

2. Framework Independence

The library has no framework dependencies, making it usable with:

  • Spring Boot
  • Ktor
  • Micronaut
  • Quarkus
  • Pure Kotlin applications

3. Testability

All interfaces enable easy testing through:

  • Mock implementations
  • In-memory implementations
  • Test doubles

4. Explicit Over Implicit

  • No magic or hidden behavior
  • Clear contracts through interfaces
  • Explicit error handling

🔧 Advanced Patterns

Transactional Outbox Pattern

suspendfuninvoke(command:CreateOrderCommand): Result<OrderId> {
return runCatching {
withTransaction {
// 1. Execute domain logicval order =Order.create(command.customerId, command.items)
// 2. Save aggregate
orderRepository.save(order)
// 3. Save events to outbox (same transaction)
order.domainEvents.forEach { event ->
outboxRepository.save(event)
}
// 4. Clear events
order.clearEvents()
order.id
}
}
}
// Separate process publishes eventsclassOutboxPublisher(
privatevaloutboxRepository:MessageOutboxRepository,
privatevaleventPublisher:DomainEventPublisher
) {
suspendfunpublishPendingEvents() {
val messages = outboxRepository.findUnpublished(limit =100)
messages.forEach { message ->try {
eventPublisher.publish(message.event)
outboxRepository.markAsPublished(message.id)
} catch (e:Exception) {
outboxRepository.incrementRetryCount(message.id)
}
}
}
}

CQRS with Separate Read Models

// Write side: Use domain modelclassCreateUserHandler : CommandHandler<CreateUserCommand, Result<UserId>> {
overridesuspendoperatorfuninvoke(command:CreateUserCommand): Result<UserId> {
return runCatching {
val user =User.create(command.email, command.name)
userRepository.save(user)
user.id
}
}
}
// Read side: Use optimized read modelclassGetUserHandler : QueryHandler<GetUserQuery, UserDto?> {
overridesuspendoperatorfuninvoke(query:GetUserQuery): UserDto? {
// Direct database query, bypassing domain modelreturn jdbcTemplate.queryForObject(
"SELECT id, email, name FROM users WHERE id = ?",
UserDto::class.java,
query.userId
)
}
}

🤖 AI Agent Support

Structus is AI-agent-friendly! We provide comprehensive resources to help AI coding assistants (GitHub Copilot, Cursor, Claude, ChatGPT, etc.) understand and properly use this library.

Quick Start for AI Agents

Point your AI assistant to the .ai/ directory for:

Example AI Prompt

I'm using the Structus library (com.melsardes.libraries.structuskotlin) to build an e-commerce platform.
Please read these files to understand the architecture:
1. .ai/library-overview.md - Core concepts
2. .ai/usage-patterns.md - Implementation patterns
3. .ai/code-templates.md - Code templates
Then help me create a new Order aggregate with the following requirements:
[describe your requirements here]

For Developers

To maximize AI assistance when using Structus:

  1. Share Context: Reference .ai/ files when asking AI for help
  2. Use Prompt Templates: Copy from .ai/prompts/ and customize
  3. Follow Patterns: AI agents trained on .ai/usage-patterns.md will generate better code
  4. Leverage Templates: Point AI to .ai/code-templates.md for boilerplate

See .ai/README.md for complete documentation.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

This library is inspired by:

  • Explicit Architecture by Herberto Graça
  • Domain-Driven Design by Eric Evans
  • Implementing Domain-Driven Design by Vaughn Vernon
  • Clean Architecture by Robert C. Martin
  • CQRS by Greg Young
  • Event Sourcing patterns

📞 Support


Made with ❤️ for the Kotlin community

About

Implements DDD patterns, CQRS with type-safe handlers, and event-driven architecture with outbox pattern. AI-agent friendly with comprehensive documentation and code templates. Framework-agnostic design. Minimal dependencies, maximum flexibility. Eliminates boilerplate and enforces best practices for building scalable, maintainable applications.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages