En esta guía (5 elementos)
Data Flow & Transformations
Data Flow & Transformations
Project-agnostic catalog. Part of the Diagram Catalog. Covers: full request lifecycle, commands vs domain entities, transformation chain.
Replace
com.example.platform/platform-serviceplaceholders with your project’s actual package and service name when applying patterns.
6. Data Flow & Transformations
6.1 Complete Request Flow: Create Organization
┌───────────────────────────────────────────────────────────────────┐│ STEP 1: HTTP REQUEST ││ ││ Client → POST /api/v1/organizations ││ Content-Type: application/json ││ { ││ "name": "Tech Innovators Inc.", ││ "contactName": "Alice", ││ "contactLastName": "Johnson", ││ "contactEmail": "alice.johnson@techinnovators.com", ││ "address": "456 Innovation Blvd" ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ STEP 2: CONTROLLER (Infrastructure Layer) ││ File: OrganizationController.kt ││ ││ @PostMapping ││ fun createOrganization( ││ @Validated @RequestBody request: CreateOrganizationRequest ││ ): ResponseEntity<CreatedOrganizationResponse> { ││ // 2.1: Bean Validation executes (@NotBlank, @Email, etc.) ││ // 2.2: Convert REST DTO → Command ││ val command = request.toCreateOrganizationCommand() ││ ││ // 2.3: Invoke inbound port ││ val organization = createOrganizationPort.execute(command) ││ ││ // 2.4: Convert Domain → Response DTO ││ return ResponseEntity.created(...) ││ .body(organization.toCreatedOrganizationResponse()) ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ STEP 3: USE CASE (Application Layer) ││ File: CreateOrganizationUseCase.kt ││ ││ class CreateOrganizationUseCase( ││ private val repository: OrganizationRepositoryPort, ││ private val transaction: TransactionPort<Organization>, ││ private val outbox: OutboxRepositoryPort, // SDK port ││ private val events: OutboxEventFactory, // SDK fact ││ ) : CreateOrganizationPort { ││ ││ override fun execute( ││ command: CreateOrganizationCommand ││ ): Organization = transaction.run { ││ // 3.1: Command → Domain Entity ││ val organization = command.toOrganization() ││ ││ // 3.2: Persist via repository port (same TX) ││ val saved = repository.save(organization) ││ ││ // 3.3: Enqueue OUTBOX row (same TX) — NO broker ││ outbox.save(events.create( ││ topic = "platform.organizations", ││ traceId = command.traceId, ││ aggregate = saved.id.toString(), ││ eventName = "OrganizationCreated", ││ eventType = EventTypes.EVENT, ││ payload = OrgCreatedPayload.from(saved), ││ )) ││ saved ││ } ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ STEP 4: TRANSACTION (Infrastructure → Application Port) ││ File: JdbcTransaction.kt ││ ││ @Component ││ class JdbcTransaction<R> : TransactionPort<R> { ││ ││ @Transactional(propagation = Propagation.REQUIRED) ││ override fun run( ││ callback: TransactionCallbackPort<R> ││ ): R { ││ try { ││ log.debug("[START] transaction") ││ val result = callback.call() ││ log.debug("[END] transaction") ││ return result ││ } catch (ex: Exception) { ││ log.error("Transaction failed", ex) ││ throw ex ││ } ││ } ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ STEP 5: REPOSITORY (Infrastructure → Application Port) ││ File: OrganizationRepositoryAdapter.kt ││ ││ @Component ││ class OrganizationRepositoryAdapter( ││ private val organizationJdbcRepository: OrganizationJdbcRepo ││ ) : OrganizationRepositoryPort { ││ ││ override fun save(organization: Organization): Organization {││ // 5.1: Domain Entity → Database Entity ││ val entity = organization.toDatabaseEntity() ││ ││ // 5.2: Save to database (Spring Data JDBC) ││ val saved = organizationJdbcRepository.save(entity) ││ ││ // 5.3: Database Entity → Domain Entity ││ return saved.toDomainModel() ││ } ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ STEP 6: HTTP RESPONSE (broker NOT yet contacted) ││ ││ HTTP 201 Created ││ Location: /api/v1/organizations/{uuid} ││ { ││ "id": "550e8400-e29b-41d4-a716-446655440000", ││ "status": "PENDING_APPROVAL" ││ } │└───────────────────────────────────────────────────────────────────┘Asynchronous lane: SDK dispatcher → broker (decoupled from request)
every outbox.task.fixed-delay (default 5s) │ ▼┌───────────────────────────────────────────────────────────────────┐│ OUTBOX SCHEDULER (codehuntersio-sdk-event-messaging) ││ ││ @Scheduled(fixedDelayString = "${outbox.task.fixed-delay:5000}") ││ @SchedulerLock( ││ name = "outbox-${spring.application.name}", ││ lockAtMostFor = "...", lockAtLeastFor = "..." ││ ) ││ fun tick() = sendOutboxMessagesPort.execute() │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ SendOutboxMessagesUseCase (SDK) ││ ││ // A.1 — atomic claim ││ val batch = outbox.claimPendingMessages( ││ origin, batchSize, instanceId, claimTtl ││ ) ││ // PENDING || stale IN_FLIGHT → IN_FLIGHT, stamp claimed_by ││ ││ for (msg in batch) { ││ try { ││ // A.2 — publish via broker-agnostic port ││ messagePublisher.publish(msg.topic, msg.routingKey, msg.message)││ msg.markPublished() ││ } catch (e) { ││ // A.3 — backoff or fail ││ msg.attempts++ ││ if (msg.attempts >= maxAttempts) msg.markFailed(e) ││ else msg.scheduleRetry(computeBackoffMs(msg.attempts)) ││ } ││ outbox.update(msg) ││ } │└────────────────────────┬──────────────────────────────────────────┘ │ ▼┌───────────────────────────────────────────────────────────────────┐│ MessagePublisherPort impl (Rabbit by default, Kafka by override) ││ ││ RabbitMessagePublisherAdapter.publish(topic, routingKey, payload)││ → rabbitTemplate.send(...) ││ ││ OR (override the bean) ││ ││ KafkaMessagePublisherAdapter.publish(topic, routingKey, payload) ││ → kafkaTemplate.send(topic, routingKey, payload).get(5, TimeUnit.SECONDS) │└───────────────────────────────────────────────────────────────────┘Two lanes, one source of truth: the synchronous lane commits the outbox row; the asynchronous lane drains it. The use case never touches the broker; the dispatcher never touches the domain.
6.2 Commands vs Domain Entities: Comparison
| Aspect | Command | Domain Entity |
|---|---|---|
| Purpose | Represent user intention | Represent business state |
| Location | Application Layer | Domain Layer |
| Properties | 5 (input only) | 14 (complete state) |
| Identity | No id field | Has id (UUID) |
| Status | No status field | Has status enum |
| Audit Fields | None | createdAt, updatedAt, createdBy, etc. |
| Validation | In REST DTO layer | Domain invariants |
| Mutability | Immutable (val) | Immutable (regular class, private ctor) |
| Business Logic | None | Methods for state transitions |
| Lifecycle | Request-scoped | Persisted, full lifecycle |
| Version Control | N/A | Optimistic locking via version |
| Soft Delete | N/A | deleted, deletedAt fields |
6.3 Why Use Commands Instead of Domain Entities?
✅ Advantages of Commands
-
Separation of Concerns
- Commands represent input (what user wants)
- Entities represent state (what system has)
- Clear boundary between user intent and domain state
-
Framework Independence
- Commands are pure Kotlin data classes
- No dependency on persistence, validation frameworks
- Easy to test in isolation
-
Explicit Contracts
- Inbound ports define clear contracts
- Changes to input structure don’t affect domain
- Versioning flexibility (v1 command → same domain entity)
-
Validation Separation
- REST layer: Syntactic validation (
@NotBlank,@Email) - Command: Pure data transfer
- Domain: Business invariants validation
- REST layer: Syntactic validation (
-
Evolution Support
- Add/remove fields in commands without touching domain
- Support multiple commands creating same entity type
- API versioning without domain changes
❌ What Happens Without Commands?
// ❌ BAD: Using Domain Entity directly in controller@PostMappingfun createOrganization( @RequestBody organization: Organization // ANTI-PATTERN!): ResponseEntity<Organization> { // Problems: // 1. Client can set id, createdAt, version → Security risk // 2. Domain entity exposed to HTTP layer → Tight coupling // 3. No clear intent → Is this create or update? // 4. Validation mixed with domain logic // 5. Can't evolve API without changing domain
return ResponseEntity.ok( createOrganizationPort.execute(organization) )}Issues:
- Security: Client can manipulate
id,version,createdAt - Coupling: Domain entity tied to HTTP contract
- Unclear Intent: Same entity for create/update/patch
- Validation Confusion: Where to validate? Controller? Domain?
- Evolution Difficulty: Can’t change API without domain impact
✅ GOOD: Using Commands
// ✅ GOOD: Using Command pattern@PostMappingfun createOrganization( @Validated @RequestBody request: CreateOrganizationRequest): ResponseEntity<CreatedOrganizationResponse> { // Benefits: // 1. Only accepts user-provided fields // 2. Domain entity created internally with system fields // 3. Clear intent: "create" operation // 4. Validation in REST layer, business rules in domain // 5. API can evolve independently
val command = request.toCreateOrganizationCommand() val organization = createOrganizationPort.execute(command)
return ResponseEntity.created(...) .body(organization.toCreatedOrganizationResponse())}Benefits:
- Security: System controls
id,version, timestamps - Decoupling: REST, Command, Domain are independent
- Clear Intent: Explicit “create” command
- Proper Validation: Syntax in REST, business in domain
- Easy Evolution: Change request/response without domain impact