Saltar al contenido
Mis Guidelines
Guías
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 @Transactional on 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 via OutboxRepositoryPort, never a XxxPublisherPort (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 definition
fun interface TransactionPort<R> {
fun run(callback: TransactionCallbackPort<R>): R
}
fun interface TransactionCallbackPort<R> {
fun call(): R
}
// Infrastructure layer - implementation
@Component
class 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 usage
class 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
@FunctionalInterface
public interface TransactionPort<R> {
R run(TransactionCallbackPort<R> callback);
}
@FunctionalInterface
public interface TransactionCallbackPort<R> {
R call();
}
// Infrastructure layer - implementation
@Component
public 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 usage
public 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

  1. Avoid NULLs
  2. Fakes > mocks
  3. Keep It Simple
  4. Use solid IDEs
  5. Names > comments
  6. Divide & conquer
  7. Write fast tests
  8. Use strong names
  9. Subtypes must fit
  10. Minimize comments
  11. Delete unused code
  12. Keep cohesion high
  13. Test early & often
  14. Master IDE hotkeys
  15. Set max line width
  16. Remove noise words
  17. Avoid magic numbers
  18. Avoid magic strings
  19. Use auto-formatters
  20. Avoid large classes
  21. Commit early & often
  22. Working ≠ clean code
  23. Comments explain why
  24. Prefix your booleans
  25. Use searchable names
  26. Don’t Repeat Yourself
  27. Avoid long conditions
  28. Write small functions
  29. Use consistent naming
  30. No extensive comments
  31. Link commits to tasks
  32. Keep interfaces small
  33. Avoid global variables
  34. Capture business logic
  35. Write repeatable tests
  36. Refactor early & often
  37. Produce thorough tests
  38. Remove not needed code
  39. Depend on abstractions
  40. Use pronounceable names
  41. Keep proper indentation
  42. Write independent tests
  43. Don’t use abbreviations
  44. Max 8-10 lines/function
  45. Use parameterized tests
  46. Strive for low coupling
  47. No horizontal alignment
  48. Use AAA pattern in tests
  49. Readability > cleverness
  50. Limit function arguments
  51. Use meaningful test data
  52. Readability > efficiency
  53. Don’t use boolean params
  54. Hard-to-test = bad smell
  55. Use formatting standards
  56. Don’t use logic in tests
  57. One responsibility/class
  58. Write meaningful commits
  59. Write deterministic tests
  60. Hide irrelevant test data
  61. Use feature-based folders
  62. Use comments for API docs
  63. Use nouns for class names
  64. Do real-time code reviews
  65. Use consistent vocabulary
  66. Avoid primitive obsession
  67. Composition > inheritance
  68. Avoid long parameter list
  69. Use Should/When test names
  70. Have one behavior per test
  71. Use descriptive test names
  72. Write self-validating tests
  73. Don’t use negative booleans
  74. Use adjectives for booleans
  75. Write clean test assertions
  76. One public method per class
  77. Pair-programming on default
  78. Don’t use encodings in names
  79. Use present tense in commits
  80. Leave code cleaner you found
  81. Reduce cyclomatic complexity
  82. One responsibility per module
  83. Write code for humans to read
  84. Use verbs for functions names
  85. Use intention-revealing names
  86. Use imperative mode in commits
  87. Use enums as flags in functions
  88. Don’t state obvious in comments
  89. Bundle data & functions together
  90. Declare variables close to usage
  91. Use empty lines to separate logic
  92. Order functions by execution order
  93. Strive for small private functions
  94. Write code that reads like a prose
  95. Use comments for implicit behaviours
  96. Use 3-second rule for function names
  97. Tests should be as clean as prod code
  98. Use rule of three to remove duplication
  99. 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 (val in Kotlin, final in Java)
  • No framework annotations in domain or application layers
  • Transaction boundaries use the Transaction port, not @Transactional on 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 class or Java POJO/record with no methods other than getters/equals/hashCode.
  • State changes happen via setStatus(...) or entity.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 private constructor or factory — any caller can build any state combination.

Rule

Every domain entity MUST:

  1. Refuse to be constructed in an invalid state (validate in constructor / init / factory).
  2. Expose intent-revealing transitions as methods (activate(), cancel(reason), transitionTo(target)), each returning a new immutable instance.
  3. Make all setters illegal — Kotlin: regular class with val + private constructor; Java: private constructor + final fields + static create(...) factory.
  4. Co-locate state-machine rules with the type that owns them (entity method or enum extension), never in a Service.
  5. 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.