En esta guía (9 elementos)
Ports & Commands
Ports & Commands
Project-agnostic catalog. Part of the Diagram Catalog. Covers: port naming conventions, command pattern analysis, three-layer mapping chain.
Replace
com.example.platform/platform-serviceplaceholders with your project’s actual package and service name when applying patterns.
4. Port Naming Conventions
4.1 Current Standards (Post-Refactoring)
All ports now follow consistent naming conventions aligned with DDD best practices:
Inbound Ports (Application Entry Points)
- Pattern:
[Action][Entity]Port - Examples:
CreateOrganizationPortGetOrganizationByIdPortChangeOrganizationStatusPortSendOutboxMessagesPort— inbound port of the outbox dispatcher (provided bycodehuntersio-sdk-event-messaging; see messaging.md §4.3.1)
- Old Pattern (Removed):
,[Action][Entity]UseCasePort(renamed toFetchOutboxMessagePortSendOutboxMessagesPort) - Rationale: “UseCase” is redundant; the port already represents the use case interface
Outbound Ports - Repositories
- Pattern:
[Entity]RepositoryPort - Examples:
OrganizationRepositoryPortUserRepositoryPortOutboxRepositoryPortWorkflowRepositoryPort
- Old Pattern (Removed):
[Entity]Repository - Rationale: Adding “Port” clarifies this is an interface, not an implementation
Outbound Ports - Publishers
- Pattern: Single broker-agnostic
MessagePublisherPortprovided bycodehuntersio-sdk-event-messaging. Do NOT create per-entity publisher ports. - Canonical port:
MessagePublisherPort(publish(topic, routingKey?, payload)) — implemented byRabbitMessagePublisherAdapter(SDK default) orKafkaMessagePublisherAdapter(override). Consumed ONLY bySendOutboxMessagesUseCase, never by application use cases. - Domain factories (not ports — pure classes in
domain/factory/):EventMessageFactory— builds theEventMessage<T>envelope, stampsoriginfromspring.application.nameOutboxEventFactory— wraps an event into anOutboxMessage<EventMessage<T>>ready to persist
- Old Patterns (Removed):
,[Entity]PublisherPort,OrganizationPublisherPort,InvoicePublisherPort— domain-specific publisher ports invited use cases to dual-write. UseOutboxPublisherPortOutboxRepositoryPort.save(...)from the use case; the SDK dispatcher does the publishing. - Rationale: Use cases never call a publisher port — only the outbox. The single
MessagePublisherPortis broker-agnostic so swapping RabbitMQ ↔ Kafka is one bean override away.
Support Ports
- Pattern:
[Concept]Port - Examples:
TransactionPort<R>TransactionCallbackPort<R>ChainablePort<T>
- Old Pattern (Removed):
Transaction<R>,TransactionCallback<R> - Rationale: Explicit port designation for infrastructure abstractions
4.2 Port Definition Guidelines
// ✅ GOOD - Inbound Portpackage com.example.platform.port.inbound
fun interface CreateOrganizationPort { fun execute(command: CreateOrganizationCommand): Organization}
// ✅ GOOD - Outbound Repository Portpackage com.example.platform.port.outbound.repository
interface OrganizationRepositoryPort : ChainablePort<OrganizationRepositoryPort> { fun save(organization: Organization): Organization fun update(organization: Organization): Organization fun findById(id: UUID): Organization?}
// ✅ GOOD - Outbound port for outbox (provided by codehuntersio-sdk-event-messaging)// Use cases inject this + OutboxEventFactory. NEVER inject a publisher port.package com.example.platform.messaging.application.port
interface OutboxRepositoryPort { fun <T> save(message: OutboxMessage<T>) fun claimPendingMessages(origin: String, batchSize: Int, claimedBy: String, claimTtl: Duration): List<OutboxMessage<String>> fun update(message: OutboxMessage<String>)}
// ❌ BAD - per-entity publisher port (invites dual-write from use case)interface OrganizationPublisherPort { fun publishCreated(organization: Organization) fun publishUpdated(organization: Organization)}
// ❌ BAD - Missing "Port" suffixinterface CreateOrganizationUseCase { ... }interface OrganizationRepository { ... }5. Command Pattern Analysis
5.1 What are Commands?
Commands are immutable data transfer objects (DTOs) that represent user intentions to change the system state. They live in the Application layer and serve as the contract between the outside world and your use cases.
5.2 Current Command Implementation
CreateOrganizationCommand
Location: application/src/main/kotlin/.../usecase/organization/command/CreateOrganizationCommand.kt
data class CreateOrganizationCommand( val name: String, val contactName: String, val contactLastName: String, val contactEmail: String, val address: String,)Characteristics:
- Pure Data Class: No validation, no business logic
- Immutable: All properties are
val - Input-Only: Represents user input before domain creation
- No Identity: No
idfield (generated later) - No Status: Status determined by business rules
5.3 Command Creation Flow
┌─────────────────────────────────────────────────────────────────┐│ 1. REST REQUEST (DTO) ││ ││ POST /api/v1/organizations ││ { ││ "name": "Acme Corp", ││ "contactName": "John", ││ "contactLastName": "Doe", ││ "contactEmail": "john.doe@acme.com", ││ "address": "123 Main St" ││ } ││ ││ ↓ Deserialized to CreateOrganizationRequest │└────────────────────────┬────────────────────────────────────────┘ │ │ .toCreateOrganizationCommand() │┌────────────────────────▼────────────────────────────────────────┐│ 2. COMMAND (Application Input DTO) ││ ││ CreateOrganizationCommand( ││ name = "Acme Corp", ││ contactName = "John", ││ contactLastName = "Doe", ││ contactEmail = "john.doe@acme.com", ││ address = "123 Main St" ││ ) ││ ││ ✓ Validation passed (in Request DTO) ││ ✓ Pure data, no business logic ││ ││ ↓ Passed to CreateOrganizationPort.execute(command) │└────────────────────────┬────────────────────────────────────────┘ │ │ CreateOrganizationUseCase receives │┌────────────────────────▼────────────────────────────────────────┐│ 3. USE CASE PROCESSING ││ ││ CreateOrganizationUseCase.execute(command) { ││ transaction.run { ││ val organization = command.toOrganization() ← MAPPER ││ val saved = repository.save(organization) ││ outbox.save(events.create(saved)) ← outbox-first ││ return saved ││ } ││ } ││ ││ ↓ command.toOrganization() via CommandMapper │└────────────────────────┬────────────────────────────────────────┘ │ │ Mapper adds: id, status, timestamps │┌────────────────────────▼────────────────────────────────────────┐│ 4. DOMAIN ENTITY (Full State) ││ ││ Organization.create( ← factory enforces ││ name = "Acme Corp", invariants ││ contactName = "John", ││ contactLastName = "Doe", ││ contactEmail = "john.doe@acme.com", ││ address = "123 Main St", ││ clock = clock, ← injected Clock ││ ) ││ // factory sets: id, status = PENDING_APPROVAL, ││ // createdAt/updatedAt = clock.instant(), version = 0 ││ ││ ✓ Complete business entity with invariants ││ ✓ Contains domain logic methods ││ ││ ↓ Saved to database via Repository │└─────────────────────────────────────────────────────────────────┘5.4 Command Mapper Implementation
Location: application/src/main/kotlin/.../usecase/organization/command/mapper/CommandMapper.kt
package com.example.platform.usecase.organization.command.mapper
import com.example.platform.domain.Organizationimport com.example.platform.usecase.organization.command.CreateOrganizationCommandimport java.time.Clock
/** * Maps CreateOrganizationCommand to Organization entity. * Delegates to the Organization.create(...) factory, which enforces * invariants and sets system-generated fields (id, status, audit * timestamps, version) using the injected Clock — never Instant.now(). */fun CreateOrganizationCommand.toOrganization(clock: Clock): Organization = Organization.create( name = this.name, contactName = this.contactName, contactLastName = this.contactLastName, contactEmail = this.contactEmail, address = this.address, clock = clock, )Mapper Responsibilities:
- ID Generation: Creates unique identifier
- Status Assignment: Sets initial business state
- Timestamp Creation: Records audit trail
- Default Values: Populates system fields
- Version Initialization: Enables optimistic locking
5.5 Three-Layer Mapping Chain
┌──────────────────────────────────────────────────────────────────┐│ LAYER 1: REST ││ CreateOrganizationRequest ││ ││ Properties: 5 fields ││ • name: String (validated: @NotBlank, @Size) ││ • contactName: String (validated: @NotBlank) ││ • contactLastName: String (validated: @NotBlank) ││ • contactEmail: String (validated: @Email) ││ • address: String (validated: @NotBlank) ││ ││ Responsibilities: ││ ✓ Input validation (Bean Validation) ││ ✓ Deserialization from JSON ││ ✓ HTTP contract definition ││ ││ ↓ Mapper: RequestMapper.toCreateOrganizationCommand() │└────────────────────────┬─────────────────────────────────────────┘ │ Strips validation, keeps pure data┌────────────────────────▼─────────────────────────────────────────┐│ LAYER 2: APPLICATION ││ CreateOrganizationCommand ││ ││ Properties: 5 fields (same as Request) ││ • name: String ││ • contactName: String ││ • contactLastName: String ││ • contactEmail: String ││ • address: String ││ ││ Responsibilities: ││ ✓ Port contract (decoupled from REST) ││ ✓ Pure data (no validation annotations) ││ ✓ Framework-independent ││ ││ ↓ Mapper: CommandMapper.toOrganization() │└────────────────────────┬─────────────────────────────────────────┘ │ Adds: id, status, timestamps, defaults┌────────────────────────▼─────────────────────────────────────────┐│ LAYER 3: DOMAIN ││ Organization ││ ││ Properties: 14 fields (complete entity) ││ • id: UUID ← Generated ││ • name: String ← From command ││ • contactName: String ← From command ││ • contactLastName: String ← From command ││ • contactEmail: String ← From command ││ • address: String ← From command ││ • status: OrganizationStatus ← Default ││ • createdAt: Instant ← Generated ││ • updatedAt: Instant ← Generated ││ • deletedAt: Instant? ← Default null ││ • createdBy: String ← Default "system" ││ • updatedBy: String ← Default "system" ││ • deleted: Boolean ← Default false ││ • version: Long ← Default 0 ││ ││ Responsibilities: ││ ✓ Business invariants enforcement ││ ✓ Domain behavior methods ││ ✓ Complete entity lifecycle ││ ││ ↓ Persisted via OrganizationRepositoryPort │└──────────────────────────────────────────────────────────────────┘