Saltar al contenido
Mis Guidelines
Guías
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-service placeholders 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:
    • CreateOrganizationPort
    • GetOrganizationByIdPort
    • ChangeOrganizationStatusPort
    • SendOutboxMessagesPort — inbound port of the outbox dispatcher (provided by codehuntersio-sdk-event-messaging; see messaging.md §4.3.1)
  • Old Pattern (Removed): [Action][Entity]UseCasePort, FetchOutboxMessagePort (renamed to SendOutboxMessagesPort)
  • Rationale: “UseCase” is redundant; the port already represents the use case interface

Outbound Ports - Repositories

  • Pattern: [Entity]RepositoryPort
  • Examples:
    • OrganizationRepositoryPort
    • UserRepositoryPort
    • OutboxRepositoryPort
    • WorkflowRepositoryPort
  • Old Pattern (Removed): [Entity]Repository
  • Rationale: Adding “Port” clarifies this is an interface, not an implementation

Outbound Ports - Publishers

  • Pattern: Single broker-agnostic MessagePublisherPort provided by codehuntersio-sdk-event-messaging. Do NOT create per-entity publisher ports.
  • Canonical port: MessagePublisherPort (publish(topic, routingKey?, payload)) — implemented by RabbitMessagePublisherAdapter (SDK default) or KafkaMessagePublisherAdapter (override). Consumed ONLY by SendOutboxMessagesUseCase, never by application use cases.
  • Domain factories (not ports — pure classes in domain/factory/):
    • EventMessageFactory — builds the EventMessage<T> envelope, stamps origin from spring.application.name
    • OutboxEventFactory — wraps an event into an OutboxMessage<EventMessage<T>> ready to persist
  • Old Patterns (Removed): [Entity]PublisherPort, OrganizationPublisherPort, InvoicePublisherPort, OutboxPublisherPort — domain-specific publisher ports invited use cases to dual-write. Use OutboxRepositoryPort.save(...) from the use case; the SDK dispatcher does the publishing.
  • Rationale: Use cases never call a publisher port — only the outbox. The single MessagePublisherPort is 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 Port
package com.example.platform.port.inbound
fun interface CreateOrganizationPort {
fun execute(command: CreateOrganizationCommand): Organization
}
// ✅ GOOD - Outbound Repository Port
package 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" suffix
interface 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 id field (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.Organization
import com.example.platform.usecase.organization.command.CreateOrganizationCommand
import 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:

  1. ID Generation: Creates unique identifier
  2. Status Assignment: Sets initial business state
  3. Timestamp Creation: Records audit trail
  4. Default Values: Populates system fields
  5. 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 │
└──────────────────────────────────────────────────────────────────┘