En esta guía (5 elementos)
Clean Code
Clean Code for Hexagonal Kotlin/Java Services
MANDATORY FOR ALL AI AGENTS: Every class, function, and module you create MUST adhere to the clean code principles below. When in doubt, favor readability over cleverness and immutability over mutability. Transaction boundaries MUST use the Transaction port abstraction, NEVER
@Transactionalon use cases.
Goals
- Keep business logic independent from frameworks and transport/persistence details.
- Small, cohesive units with clear responsibilities. Prefer composition over inheritance.
Principles
- Single Responsibility: each class has one reason to change.
- Open/Closed: extend via new classes/adapters; do not modify domain for infrastructure needs.
- Dependency Inversion: domain and application declare ports; infrastructure implements them.
- Tell, Don’t Ask: encapsulate behavior inside domain objects/use cases.
- YAGNI/KISS: do the simplest thing that works; avoid speculative generalization.
- Immutability: prefer immutable domain models and DTOs.
Layers in this repo
- Domain (pure): entities/value objects, enums, domain services if any. No Spring, no I/O.
- Application (use cases): orchestrate domain logic, define inbound/outbound ports, enforce application rules. No persistence/messaging/web.
- Infrastructure (adapters): implementations of outbound ports (DB, Kafka), and entry points (REST/GraphQL/Kafka consumer). Contains frameworks and I/O.
- Bootstrap: application wiring and configuration to assemble the runtime.
Use cases
- Keep as thin as possible but the single place for orchestration and transactions.
- Depend only on ports defined in application.
- Validate inputs and enforce application rules; domain invariants live in domain.
Ports
- Inbound ports: interfaces representing use case API consumed by entry points.
- Outbound ports: interfaces representing dependencies needed by use cases.
- Suffixes: inbound ports use
[Action][Entity]Port(e.g.CreateOrganizationPort); outbound repository ports use[Entity]RepositoryPort(e.g.OrganizationRepositoryPort). No per-entity publisher ports — use cases enqueue events viaOutboxRepositoryPort, never aXxxPublisherPort(the ArchUnit rule bans it).
Adapters
- Entry points (REST/GraphQL/Kafka): map transport to inbound ports, handle validation and response mapping, no business logic.
- Driven adapters (JDBC/Kafka producer): implement outbound ports, map domain to technology-specific types, handle technical errors.
Mapping
- Create dedicated mappers for each boundary (REST, GraphQL, JDBC, Kafka). Keep pure functions.
- Do not place mapping logic inside use cases or domain objects.
Transactions
- DO NOT annotate use cases with
@Transactional. Keep application layer framework-free. - Define transaction abstraction as a port (interface) in application layer.
- Implement transaction port in infrastructure layer with Spring’s
@Transactional. - Use lambda callbacks to control transaction boundaries: Kotlin:
// Application layer - port definitionfun interface TransactionPort<R> { fun run(callback: TransactionCallbackPort<R>): R}
fun interface TransactionCallbackPort<R> { fun call(): R}
// Infrastructure layer - implementation@Componentclass JdbcTransaction<R> : TransactionPort<R> { @Transactional( propagation = Propagation.REQUIRED, rollbackFor = [Exception::class] // Rollback on ALL exceptions ) override fun run(callback: TransactionCallbackPort<R>): R { return try { callback.call() } catch (exception: SQLException) { log.error("Database error in transaction", exception) throw exception } catch (exception: Exception) { log.error("Unexpected error in transaction", exception) throw RuntimeException("Transaction failed: ${exception.message}", exception) } }}
// Use case usageclass CreateOrganizationUseCase( private val transaction: TransactionPort<Organization>, private val repository: OrganizationRepositoryPort) : CreateOrganizationPort { override fun execute(command: CreateOrganizationCommand): Organization { return transaction.run { val org = repository.save(command.toOrganization()) return@run org } }}Java:
// Application layer - port definition@FunctionalInterfacepublic interface TransactionPort<R> { R run(TransactionCallbackPort<R> callback);}
@FunctionalInterfacepublic interface TransactionCallbackPort<R> { R call();}
// Infrastructure layer - implementation@Componentpublic class JdbcTransaction<R> implements TransactionPort<R> { private static final Logger log = LoggerFactory.getLogger(JdbcTransaction.class);
@Override @Transactional( propagation = Propagation.REQUIRED, rollbackFor = Exception.class ) public R run(TransactionCallbackPort<R> callback) { try { return callback.call(); } catch (SQLException exception) { log.error("Database error in transaction", exception); throw exception; } catch (Exception exception) { log.error("Unexpected error in transaction", exception); throw new RuntimeException("Transaction failed: " + exception.getMessage(), exception); } }}
// Use case usagepublic class CreateOrganizationUseCase implements CreateOrganizationPort { private final TransactionPort<Organization> transaction; private final OrganizationRepositoryPort repository;
public CreateOrganizationUseCase(TransactionPort<Organization> transaction, OrganizationRepositoryPort repository) { this.transaction = transaction; this.repository = repository; }
@Override public Organization execute(CreateOrganizationCommand command) { return transaction.run(() -> { return repository.save(CommandMapper.toOrganization(command)); }); }}- IMPORTANT: Use
rollbackFor = [Exception::class]to ensure ALL exceptions trigger rollback, not just SQLException - Framework annotations (
@Transactional) only in infrastructure, never in domain or application layers
Errors
- Prefer meaningful exceptions or sealed error results in domain/application.
- Centralize exception-to-HTTP/GraphQL error mapping in handlers.
Testing strategy
- Domain: pure unit tests without mocks.
- Use cases: unit tests mocking outbound ports; verify behavior and interactions.
- Adapters: slice/integration tests (e.g., MyBatis with Testcontainers), and contract tests for REST/GraphQL.
- End-to-end (optional): thin happy path flows using docker-compose/Testcontainers.
Code smells to avoid
- Anemic domain where all logic is in controllers or repositories.
- Leaking Spring or persistence annotations into domain/application modules.
- God services with many responsibilities; break down by use case.
- Overuse of static utility; prefer domain services or extension functions scoped appropriately.
Tips
- Avoid NULLs
- Fakes > mocks
- Keep It Simple
- Use solid IDEs
- Names > comments
- Divide & conquer
- Write fast tests
- Use strong names
- Subtypes must fit
- Minimize comments
- Delete unused code
- Keep cohesion high
- Test early & often
- Master IDE hotkeys
- Set max line width
- Remove noise words
- Avoid magic numbers
- Avoid magic strings
- Use auto-formatters
- Avoid large classes
- Commit early & often
- Working ≠ clean code
- Comments explain why
- Prefix your booleans
- Use searchable names
- Don’t Repeat Yourself
- Avoid long conditions
- Write small functions
- Use consistent naming
- No extensive comments
- Link commits to tasks
- Keep interfaces small
- Avoid global variables
- Capture business logic
- Write repeatable tests
- Refactor early & often
- Produce thorough tests
- Remove not needed code
- Depend on abstractions
- Use pronounceable names
- Keep proper indentation
- Write independent tests
- Don’t use abbreviations
- Max 8-10 lines/function
- Use parameterized tests
- Strive for low coupling
- No horizontal alignment
- Use AAA pattern in tests
- Readability > cleverness
- Limit function arguments
- Use meaningful test data
- Readability > efficiency
- Don’t use boolean params
- Hard-to-test = bad smell
- Use formatting standards
- Don’t use logic in tests
- One responsibility/class
- Write meaningful commits
- Write deterministic tests
- Hide irrelevant test data
- Use feature-based folders
- Use comments for API docs
- Use nouns for class names
- Do real-time code reviews
- Use consistent vocabulary
- Avoid primitive obsession
- Composition > inheritance
- Avoid long parameter list
- Use Should/When test names
- Have one behavior per test
- Use descriptive test names
- Write self-validating tests
- Don’t use negative booleans
- Use adjectives for booleans
- Write clean test assertions
- One public method per class
- Pair-programming on default
- Don’t use encodings in names
- Use present tense in commits
- Leave code cleaner you found
- Reduce cyclomatic complexity
- One responsibility per module
- Write code for humans to read
- Use verbs for functions names
- Use intention-revealing names
- Use imperative mode in commits
- Use enums as flags in functions
- Don’t state obvious in comments
- Bundle data & functions together
- Declare variables close to usage
- Use empty lines to separate logic
- Order functions by execution order
- Strive for small private functions
- Write code that reads like a prose
- Use comments for implicit behaviours
- Use 3-second rule for function names
- Tests should be as clean as prod code
- Use rule of three to remove duplication
- Strive for no side effects in functions
Post-Generation Verification Checklist
After generating any code, verify:
- No class has more than one responsibility
- No function exceeds 10 lines (aim for 8-10 max)
- No function has more than 3 parameters
- All variable names are intention-revealing
- No magic numbers or strings exist (use constants)
- Domain models are immutable (
valin Kotlin,finalin Java) - No framework annotations in domain or application layers
- Transaction boundaries use the Transaction port, not
@Transactionalon use cases - Mapping logic is in dedicated mapper classes, not in use cases or domain objects
- Error handling uses domain-specific exceptions, not generic ones
- Tests follow Given-When-Then structure
- No business logic in controllers or adapters
- No anemic domain models — every aggregate root and entity owns its invariants and state transitions (see below)
Anti-Pattern: Anemic Domain Model
A class lives in domain/ but contains only fields + accessors; all business logic sits in a service or use case. Per clean-code (Fowler) and DDD (Evans), this defeats encapsulation — the model is a data bag, the logic is procedural, and the invariants are unenforceable from inside the type.
Symptoms
- The class is a Kotlin
data classor Java POJO/record with no methods other than getters/equals/hashCode. - State changes happen via
setStatus(...)orentity.copy(status = X)called from outside the class. - Use cases contain conditionals like
if (entity.status == X) ... else throw. - Two services modify the same entity with subtly different validation rules.
- The class has no
privateconstructor or factory — any caller can build any state combination.
Rule
Every domain entity MUST:
- Refuse to be constructed in an invalid state (validate in constructor /
init/ factory). - Expose intent-revealing transitions as methods (
activate(),cancel(reason),transitionTo(target)), each returning a new immutable instance. - Make all setters illegal — Kotlin: regular
classwithval+privateconstructor; Java:privateconstructor +finalfields +static create(...)factory. - Co-locate state-machine rules with the type that owns them (entity method or enum extension), never in a
Service. - Use value objects (
Email,Money,OrganizationName) instead of raw primitives — primitive obsession is anemia’s pre-stage.
Quick fix recipe
| Before (anemic) | After (rich) |
|---|---|
data class Organization(... val status: OrganizationStatus) |
class Organization private constructor(...) { fun activate(): Organization = ... ; companion object { fun create(...) = ... } } |
service.activate(org) mutates externally |
org.activate(clock) returns a new instance; service only orchestrates load → behavior → save |
setStatus(...) exposed |
No setter; one method per legal transition |
String email field |
Email(String) value object with init { require(...) } |
See ddd.md §11.1 — Anti-pattern: Anemic Domain Model for the long form with examples in both languages.