Series MapLesson 02 / 32
Start HereOrdered learning track

Learn Java Data Mapper Json Xml Validation Part 002 Data Boundary Mental Model

18 min read3543 words
PrevNext
Lesson 0232 lesson track0106 Start Here

title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 002 description: Mental model data boundary untuk membedakan DTO, entity, command, event, API contract, read model, dan validation/mapping responsibility di sistem Java production. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 2 partTitle: Data Boundary Mental Model tags:

  • java
  • data-boundary
  • dto
  • api-contract
  • event-contract
  • command
  • entity
  • jackson
  • mapstruct
  • validation date: 2026-06-29

Part 002 — Data Boundary Mental Model

1. Tujuan Part Ini

Part ini menjawab pertanyaan fundamental:

Model apa yang seharusnya dipakai di boundary, model apa yang seharusnya tetap internal, dan kapan data harus dimapping?

Kesalahan besar dalam sistem Java enterprise bukan sekadar salah konfigurasi Jackson atau MapStruct. Kesalahan besarnya adalah mencampur model yang punya alasan hidup berbeda.

Contoh anti-pattern:

@RestController
class CaseController {

    @PostMapping("/cases")
    CaseEntity create(@RequestBody CaseEntity entity) {
        return repository.save(entity);
    }
}

Ini ringkas. Ini juga berbahaya.

Masalahnya:

  • persistence entity menjadi API contract;
  • database structure bocor ke client;
  • field internal bisa diisi client;
  • lazy-loading/cycle bisa ikut serialize;
  • migration database menjadi breaking change API;
  • validation database dan validation external tercampur;
  • domain invariant tidak punya boundary jelas;
  • security review menjadi sulit.

Part ini membangun mental model untuk menghindari itu.


2. Boundary: Definisi Praktis

Boundary adalah titik ketika data berpindah dari satu ownership context ke ownership context lain.

Boundary tidak selalu berarti network.

Contoh boundary:

BoundaryDariKe
HTTP requestexternal clientservice
HTTP responseserviceexternal client
message eventproducer serviceconsumer service
database rowdatabaseapplication
commandapplication layerdomain/use case
query resultdatabase/read sideUI/API response
file importfile system/vendoringestion service
XML exchangelegacy systemintegration layer
scheduled job inputscheduler/configjob handler
validation resultvalidation layererror response model

Setiap boundary punya contract.

Contract menjawab:

  • field apa yang ada;
  • tipe apa yang diterima;
  • field mana required;
  • field mana optional;
  • null berarti apa;
  • unknown field diapakan;
  • format date-time apa;
  • enum boleh apa saja;
  • error apa yang dikembalikan;
  • perubahan apa yang dianggap breaking.

3. Model Bukan Sekadar Class

Di Java, semuanya bisa terlihat seperti class/record. Tetapi class dengan shape yang mirip belum tentu punya peran yang sama.

Contoh:

public record CaseDto(
        String id,
        String status,
        String description
) {}
public record Case(
        CaseId id,
        CaseStatus status,
        CaseDescription description
) {}
@Entity
class CaseEntity {
    @Id
    String id;

    String status;

    @Column(length = 4000)
    String description;
}

Tiga model ini mirip. Tetapi alasan hidupnya berbeda.

ModelAlasan hidup
DTOcontract dengan consumer/producer tertentu
Domain modelmenjaga invariant dan behavior bisnis
Entitypersistence mapping dan lifecycle database
Eventfakta yang sudah terjadi dan dikonsumsi async
Commandintent untuk melakukan perubahan
Query/read modelbentuk data optimal untuk baca/display
XML modelrepresentasi contract XML, sering namespace/schema-driven
Error modelcontract kegagalan yang bisa dipahami client

Kesalahan umum: karena field-nya sama, engineer menganggap modelnya boleh sama.

Padahal model tidak hanya ditentukan oleh field. Model ditentukan oleh ownership, lifecycle, invariant, compatibility, dan trust level.


4. Peta Model dalam Aplikasi Production

Aturan inti:

Model boleh mirip. Ownership tidak boleh kabur.


5. DTO: Data Transfer Object

DTO adalah model contract. DTO tidak harus punya behavior bisnis. DTO harus mudah diserialisasi/deserialisasi dan stabil terhadap consumer.

DTO yang baik:

  • eksplisit terhadap external field name;
  • jelas terhadap null/absence policy;
  • tidak membawa field internal;
  • tidak mengandung lazy-loaded graph;
  • tidak menyembunyikan business rule;
  • punya validation structural;
  • punya fixture contract;
  • punya compatibility policy.

Contoh request DTO:

package com.example.caseintake.api.v1;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.time.OffsetDateTime;
import java.util.List;

public record CaseIntakeRequest(
        @NotBlank
        String externalReference,

        @Valid
        @NotNull
        ReporterRequest reporter,

        @NotNull
        CaseTypeRequest caseType,

        PriorityRequest priority,

        @NotNull
        OffsetDateTime occurredAt,

        @NotBlank
        @Size(max = 4000)
        String description,

        List<AttachmentRequest> attachments
) {
}

Contoh response DTO:

package com.example.caseintake.api.v1;

import java.time.OffsetDateTime;

public record CaseIntakeResponse(
        String caseId,
        String externalReference,
        String status,
        OffsetDateTime acceptedAt
) {
}

Perhatikan:

  • request DTO dan response DTO dipisah;
  • request tidak punya caseId internal;
  • response tidak perlu mengembalikan semua field input;
  • field status output berasal dari sistem, bukan dari client.

6. Command: Intent untuk Mengubah Sistem

Command bukan external contract. Command adalah intent internal application layer.

DTO menjawab:

Client mengirim apa?

Command menjawab:

Sistem diminta melakukan apa?

Contoh:

package com.example.caseintake.application;

import java.time.OffsetDateTime;
import java.util.List;

public record OpenCaseCommand(
        ExternalReference externalReference,
        Reporter reporter,
        RequestedCaseType caseType,
        RequestedPriority requestedPriority,
        OffsetDateTime occurredAt,
        CaseDescription description,
        List<AttachmentReference> attachments,
        Actor actor
) {
}

Command bisa punya data yang tidak berasal dari request body:

  • authenticated actor;
  • tenant id;
  • request id;
  • channel;
  • correlation id;
  • resolved default;
  • normalized value;
  • idempotency key.

Itulah sebabnya RequestDto tidak selalu sama dengan Command.

Mapping dari DTO ke command adalah semantic mapping.

@Mapper(componentModel = "spring")
public interface CaseIntakeMapper {

    OpenCaseCommand toCommand(
            CaseIntakeRequest request,
            Actor actor,
            Channel channel
    );
}

Dalam praktik, method seperti di atas sering butuh @Context, factory, atau manual assembler karena tidak semua field berasal dari satu source.


7. Domain Model: Invariant dan Behavior

Domain model bukan bentuk data untuk JSON. Domain model adalah tempat invariant hidup.

Contoh:

public final class CaseDescription {
    private final String value;

    private CaseDescription(String value) {
        this.value = value;
    }

    public static CaseDescription of(String raw) {
        String normalized = raw == null ? "" : raw.trim();

        if (normalized.isBlank()) {
            throw new IllegalArgumentException("description must not be blank");
        }

        if (normalized.length() > 4000) {
            throw new IllegalArgumentException("description must not exceed 4000 characters");
        }

        return new CaseDescription(normalized);
    }

    public String value() {
        return value;
    }
}

Mungkin terlihat mirip dengan @NotBlank dan @Size(max = 4000) di DTO. Tetapi tujuannya beda.

  • DTO validation memberi feedback cepat ke client.
  • Domain invariant memastikan object internal tidak bisa berada pada state invalid.

Untuk sistem serius, keduanya bisa hidup bersamaan.

Jangan berpikir:

“Sudah ada @NotBlank, domain tidak perlu validate.”

Lebih aman berpikir:

“DTO validation menolak input buruk. Domain invariant melindungi sistem dari semua caller, termasuk test, batch job, migration, dan future code.”


8. Entity: Persistence Mapping, Bukan API Contract

Entity hidup karena database. Entity punya concern yang berbeda:

  • table/column mapping;
  • id generation;
  • optimistic locking;
  • lazy loading;
  • relationship;
  • persistence lifecycle;
  • migration compatibility;
  • database constraints.

Contoh:

@Entity
@Table(name = "case_record")
class CaseEntity {

    @Id
    @Column(name = "case_id")
    private String id;

    @Column(name = "external_ref", nullable = false)
    private String externalReference;

    @Column(name = "status", nullable = false)
    private String status;

    @Column(name = "description", nullable = false, length = 4000)
    private String description;

    @Version
    private long version;

    protected CaseEntity() {
    }

    // factory/update methods omitted
}

Entity tidak boleh otomatis menjadi response DTO.

Alasannya:

Entity concernRisiko jika diekspos ke API
lazy relationserialization error/cycle/N+1
internal iddata leak atau enumeration risk
version fieldconcurrency detail bocor
soft delete flagimplementation detail bocor
audit columnuser/internal metadata bocor
schema migrationAPI ikut berubah
bidirectional relationinfinite recursion
JPA proxyserialization unpredictable

Kita tidak membahas JPA detail di seri ini, tetapi kita harus tahu batasnya karena mapper sering berdiri di antara entity dan DTO.


9. Event Contract

Event berbeda dari response.

Response adalah jawaban langsung ke caller.

Event adalah fakta yang dipublikasikan untuk consumer lain, sering async, sering disimpan lama, dan sering lebih sulit diubah.

Contoh event:

public record CaseOpenedEventV1(
        String eventId,
        String caseId,
        String externalReference,
        String caseType,
        String occurredAt,
        String openedAt
) {
}

Event contract harus lebih hati-hati karena:

  • consumer bisa banyak dan tidak semua diketahui;
  • event bisa disimpan di log/outbox/topic lama;
  • replay event lama harus tetap bisa diproses;
  • field removal sangat berisiko;
  • enum drift bisa mematahkan consumer;
  • timestamp semantics harus jelas.

Rule of thumb:

Treat event DTO as a long-lived public contract, even inside company.

Mapping domain event ke integration event harus eksplisit.

@Mapper(componentModel = "spring")
public interface CaseEventMapper {
    CaseOpenedEventV1 toIntegrationEvent(CaseOpened domainEvent);
}

Jangan langsung serialize domain event jika domain event mengandung object internal, value object kompleks, atau field yang belum punya compatibility policy.


10. Read Model / View Model

Read model adalah model untuk kebutuhan baca. Ia bisa berbeda dari domain dan entity.

Contoh:

public record CaseSummaryResponse(
        String caseId,
        String title,
        String statusLabel,
        String priorityLabel,
        String reporterName,
        String openedAtDisplay
) {
}

Read model boleh punya field display-oriented. Tetapi perlu hati-hati:

  • jangan campur localization terlalu dini;
  • jangan ubah machine-readable value menjadi label jika consumer butuh logic;
  • jangan expose internal status jika external status mapping berbeda;
  • jangan serialize field agregat yang mahal tanpa sadar.

Read model mapping sering terlihat sederhana, tetapi sebenarnya banyak semantic decision:

public record CaseSummaryProjection(
        String caseId,
        String statusCode,
        String priorityCode,
        String reporterName,
        OffsetDateTime openedAt
) {
}
public record CaseSummaryResponse(
        String caseId,
        String status,
        String priority,
        String reporterName,
        String openedAt
) {
}

Pertanyaan:

  • apakah status response pakai internal code atau external code?
  • apakah openedAt harus ISO-8601 atau localized string?
  • apakah priority hasil request atau hasil kalkulasi sistem?
  • apakah reporterName sensitif?

11. Error Model: Contract Saat Gagal

Error response juga contract.

Jangan biarkan exception internal langsung menjadi API response.

Contoh error DTO:

public record ApiErrorResponse(
        String code,
        String message,
        String correlationId,
        List<FieldViolationResponse> violations
) {
}
public record FieldViolationResponse(
        String field,
        String code,
        String message,
        Object rejectedValue
) {
}

Tetapi rejectedValue berbahaya jika field sensitif.

Lebih aman:

public record FieldViolationResponse(
        String field,
        String code,
        String message
) {
}

Failure taxonomy:

Setiap failure type harus punya handling berbeda.

FailureBiasanya HTTPContoh
Parse error400JSON malformed
Deserialization error400enum tidak dikenal
Validation error400/422field required kosong
Mapping error400/422/500 tergantung sebabexternal code tidak punya mapping
Business rejection409/422case sudah ada, transition illegal
Internal serialization error500response gagal dibentuk

Status HTTP bisa bervariasi sesuai standar organisasi, tetapi taxonomy-nya harus jelas.


12. Trust Boundary

Mapping harus sadar trust.

Data dari client tidak otomatis trusted. Bahkan data dari service internal lain pun hanya trusted sesuai contract.

Contoh field dengan trust issue:

FieldRisiko
userIdclient impersonation
tenantIdcross-tenant access
statusclient mengatur state internal
roleprivilege escalation
createdAtaudit manipulation
pricetampering
prioritybypass triage
isVerifiedtrust escalation
sourceSystemspoofing
correlationIdlog injection jika tidak dibatasi

Aturan:

Field yang menentukan authorization, ownership, audit, atau lifecycle kritis sebaiknya tidak diambil mentah dari request body.

Mapper harus sering menggabungkan body dengan context:

public record CreateCaseHttpInput(
        CaseIntakeRequest body,
        AuthenticatedActor actor,
        TenantId tenantId,
        RequestMetadata metadata
) {
}

Lalu:

public record OpenCaseCommand(
        TenantId tenantId,
        ActorId actorId,
        ExternalReference externalReference,
        Reporter reporter,
        RequestedPriority requestedPriority,
        RequestMetadata metadata
) {
}

13. Contract Axes

Saat mendesain model boundary, jangan hanya lihat field. Evaluasi tujuh axis berikut.

AxisPertanyaan
ShapeStruktur field/object/array seperti apa?
TypeTipe data external dan internal apa?
MeaningField berarti apa dalam domain?
LifecycleKapan field dibuat, berubah, deprecated, atau dihapus?
OwnershipSiapa yang boleh mengisi atau mengubah field?
CompatibilityApa yang terjadi pada client lama dan baru?
FailureJika invalid, error apa yang muncul?

Contoh priority.

AxisKeputusan
Shapestring enum
Typeexternal enum LOW/NORMAL/HIGH/URGENT
Meaningrequested priority, bukan final priority
Lifecycleoptional di v1, mungkin required di v2
Ownershipclient boleh request, sistem boleh override
Compatibilityabsent default NORMAL; unknown rejected
Failureinvalid enum -> deserialization/validation error

Dengan matrix ini, mapper menjadi jelas:

  • PriorityRequest tidak sama dengan domain Priority;
  • field di command harus bernama requestedPriority;
  • domain service menentukan final priority.

14. Null vs Absence di Boundary

Ini salah satu topik terbesar dan akan punya part sendiri. Namun mental model-nya harus dimulai sekarang.

JSON bisa membedakan:

{}

dan:

{
  "priority": null
}

Tetapi Java object biasa sering kehilangan perbedaan itu jika langsung deserialized ke field nullable.

Dalam create request:

InputKemungkinan makna
field absentgunakan default
field nullinvalid
field empty stringinvalid atau clear?
field empty arraysengaja kosong
field absent arraytidak dikirim, mungkin default empty
field object {}object ada tetapi isinya kosong

Dalam patch request:

InputKemungkinan makna
field absentjangan ubah
field nullclear value
field valueset value

Karena itu create DTO dan patch DTO sering harus berbeda.

public record CreateCaseRequest(
        String description,
        PriorityRequest priority
) {
}
public record PatchCaseRequest(
        JsonNullable<String> description,
        JsonNullable<PriorityRequest> priority
) {
}

JsonNullable hanya contoh konsep. Library/approach yang dipakai harus dipilih sesuai stack. Yang penting: PATCH perlu merepresentasikan tri-state:

  1. absent;
  2. explicit null;
  3. present value.

Jika tidak, mapper tidak bisa tahu intent client.


15. Boundary Model Decision Table

SituationModel yang disarankanAlasan
Public HTTP requestRequest DTOcontract stabil, validasi jelas
Public HTTP responseResponse DTOhindari leak internal
Internal commandCommand objectmewakili intent use case
Domain invariantDomain model/value objectmenjaga correctness
Database mappingEntity/record persistenceschema/lifecycle DB
Message eventEvent DTO/versioned eventcompatibility async
XML legacy integrationXML DTO/JAXB modelnamespace/schema-driven
Large import fileStreaming record modelhemat memory
UI-specific readRead model/response projectionoptimize query/display
Error responseError DTOfailure contract stabil

16. Where Mapping Lives

Mapping harus berada di boundary antar model, bukan tersebar random.

api/
  CaseController.java
  dto/
    CaseIntakeRequest.java
    CaseIntakeResponse.java
  mapper/
    CaseApiMapper.java

application/
  OpenCaseCommand.java
  OpenCaseUseCase.java

domain/
  Case.java
  CaseStatus.java
  CaseDescription.java

infrastructure/
  persistence/
    CaseEntity.java
    CasePersistenceMapper.java
  messaging/
    CaseEventMapper.java

Aturan praktis:

MappingLokasi
Request DTO → CommandAPI/application boundary
Command → Domain value objectapplication/domain factory
Domain → Response DTOAPI mapper
Entity → Domaininfrastructure mapper/repository
Domain → Entityinfrastructure mapper/repository
Domain event → Integration eventmessaging/outbox mapper
External service DTO → internal modelintegration adapter

Jangan membuat satu GlobalMapper yang tahu semua hal.

Contoh buruk:

@Component
public class CaseMapper {
    CaseEntity requestToEntity(CaseIntakeRequest request) { ... }
    CaseIntakeResponse entityToResponse(CaseEntity entity) { ... }
    CaseOpenedEvent entityToEvent(CaseEntity entity) { ... }
}

Masalah:

  • request langsung ke entity;
  • entity menjadi pusat semua mapping;
  • domain dilewati;
  • mapper tahu terlalu banyak;
  • perubahan satu boundary merusak boundary lain.

Lebih baik:

CaseApiMapper:
  CaseIntakeRequest -> OpenCaseCommand
  CaseCreatedResult -> CaseIntakeResponse

CasePersistenceMapper:
  Case -> CaseEntity
  CaseEntity -> Case

CaseIntegrationEventMapper:
  CaseOpened -> CaseOpenedEventV1

17. Jackson, MapStruct, Validation dalam Boundary

Tools bekerja pada tahap berbeda.

Tanggung jawab:

StageToolFailure yang mungkin
parseJackson Core/XML parsermalformed payload
deserializeJackson Databind/JAXBtype mismatch, unknown enum
validate DTOJakarta Validation/Hibernate Validatorconstraint violation
mapMapStruct/manual mapperunmapped semantic, unsupported value
execute domaindomain model/use caseinvariant violation, conflict
serializeJackson/JAXBobject graph/cycle/custom serializer error

18. Example: Bad Boundary vs Good Boundary

18.1 Bad Boundary

@PostMapping("/cases")
public CaseEntity create(@RequestBody @Valid CaseEntity entity) {
    return caseRepository.save(entity);
}

Problems:

  • external client can set entity fields;
  • validation annotations serve persistence and API at same time;
  • entity relationship may serialize accidentally;
  • database column names influence API shape;
  • audit fields may leak;
  • future schema change becomes API change.

18.2 Better Boundary

@PostMapping("/cases")
public CaseIntakeResponse create(
        @RequestBody @Valid CaseIntakeRequest request,
        Authentication authentication
) {
    Actor actor = actorResolver.resolve(authentication);
    OpenCaseCommand command = mapper.toCommand(request, actor);
    CaseCreatedResult result = openCaseUseCase.open(command);
    return mapper.toResponse(result);
}

Better because:

  • request DTO is external contract;
  • actor comes from security context, not body;
  • command expresses internal intent;
  • use case owns business decision;
  • response DTO controls output contract.

19. Validation Placement

Validation is layered.

Contoh placement:

RulePlacement
description must not be blankDTO + domain value object
description max 4000DTO + domain/database
email formatDTO
externalReference unique per partnerapplication/domain service + database unique constraint
status transition alloweddomain
createdAt generated by systemapplication/domain, not request
tenant must match actorapplication authorization
foreign id existsapplication/domain repository check
XML syntax validparser
JSON field unknown allowed?Jackson config/contract test

Rule of thumb:

Use Jakarta Validation for local structural constraints. Use domain/application code for rules requiring state, authorization, lifecycle, or cross-aggregate knowledge.


20. Contract Evolution Mental Model

DTO changes are not all equal.

ChangeUsually safe?Notes
Add optional request fieldusually safeif unknown fields ignored by old server/client
Add response fieldusually safeif clients tolerate extra fields
Remove fieldrisky/breakingclients may depend on it
Rename fieldbreakingunless alias/versioning used
Change typebreakingstring to object, number to string
Tighten validationpotentially breakingold valid requests become invalid
Loosen validationoften saferbut can affect domain
Add enum valueriskyclients may not handle it
Change defaultriskybehavior changes silently
Change null semanticsvery riskycan corrupt intent

Kunci:

Compatibility is about observed behavior, not just compile-time shape.


21. Anti-Patterns

21.1 Entity as DTO

@RequestBody CaseEntity entity

Avoid.

21.2 One Model for Everything

class CaseModel {
    // used for request, response, entity, event, command
}

Avoid unless system sangat kecil dan risikonya sadar.

21.3 Mapper as Business Engine

@Mapping(target = "status", expression = "java(decideStatus(request))")

Jika decideStatus adalah business policy kompleks, jangan sembunyikan di mapper.

21.4 Validation as Domain Replacement

public record TransferRequest(
    @Positive BigDecimal amount
) {}

Ini tidak menjamin domain transfer valid. Domain masih perlu rules seperti balance, account status, limit, currency, fraud policy.

21.5 Silent Default Everywhere

public Priority priority() {
    return priority == null ? Priority.NORMAL : priority;
}

Default tersembunyi membuat audit sulit. Default harus ada di tempat yang bisa diuji dan dijelaskan.

21.6 Unknown Field Policy Tanpa Kesadaran

objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

Ini bukan selalu salah. Yang salah adalah melakukannya tanpa contract policy.

21.7 Exposing Internal Enum Directly

public enum CaseStatus {
    DRAFT,
    PENDING_TRIAGE,
    AUTO_ESCALATED,
    CLOSED_BY_MIGRATION
}

Jika enum ini keluar ke public API, setiap internal state menjadi public contract.


22. Boundary Review Checklist

Gunakan checklist ini saat review PR.

Model Ownership

  • Apakah model ini request, response, command, domain, entity, event, atau read model?
  • Apakah nama package mencerminkan perannya?
  • Apakah model internal bocor ke external boundary?
  • Apakah model external masuk terlalu jauh ke domain?

Field Semantics

  • Apakah setiap field punya makna jelas?
  • Apakah field system-owned tidak bisa diisi client?
  • Apakah field audit tidak berasal dari request body?
  • Apakah enum external dipisah dari enum internal?
  • Apakah timestamp punya timezone/offset policy?

Null/Absence

  • Apakah required/optional jelas?
  • Apakah null berbeda dari absent?
  • Apakah default eksplisit?
  • Apakah PATCH memakai tri-state?

Validation

  • Apakah structural validation ada di DTO?
  • Apakah domain invariant tetap ada?
  • Apakah cross-field rule ditempatkan benar?
  • Apakah rule yang butuh database tidak dipaksa masuk annotation field?

Mapping

  • Apakah mapper hanya mengubah bentuk atau juga menyembunyikan policy?
  • Apakah mapper test mencakup null/absence/default?
  • Apakah field sensitif tidak ikut map ke response/event?
  • Apakah unmapped target policy cukup ketat?

Compatibility

  • Apakah perubahan ini additive atau breaking?
  • Apakah fixture v1/v2 diperbarui?
  • Apakah unknown field behavior jelas?
  • Apakah enum baru aman untuk consumer?

Failure

  • Apakah parse/deserialization/validation/mapping/domain errors dibedakan?
  • Apakah error response tidak membocorkan sensitive value?
  • Apakah correlation id tersedia?
  • Apakah log tidak menyimpan payload sensitif mentah?

23. Practice: Desain Boundary untuk Case Intake

Buat struktur package:

com.example.caseintake
  api
    v1
      CaseIntakeRequest.java
      CaseIntakeResponse.java
      ReporterRequest.java
      AttachmentRequest.java
      CaseTypeRequest.java
      PriorityRequest.java
      CaseErrorResponse.java
  application
    OpenCaseCommand.java
    OpenCaseUseCase.java
    CaseCreatedResult.java
  domain
    Case.java
    CaseId.java
    ExternalReference.java
    Reporter.java
    CaseDescription.java
    CaseType.java
    Priority.java
  infrastructure
    persistence
      CaseEntity.java
      CasePersistenceMapper.java
    messaging
      CaseOpenedEventV1.java
      CaseEventMapper.java

Lalu tulis mapping responsibility:

# Mapping Responsibility

## API
- CaseIntakeRequest -> OpenCaseCommand
- CaseCreatedResult -> CaseIntakeResponse
- ConstraintViolation -> CaseErrorResponse

## Domain
- OpenCaseCommand -> Case creation behavior
- domain validates lifecycle and invariant

## Persistence
- Case -> CaseEntity
- CaseEntity -> Case

## Messaging
- CaseOpened -> CaseOpenedEventV1

24. Mini Case Study: Priority Field

Requirement

Client boleh mengirim priority, tetapi sistem boleh override berdasarkan policy.

Payload:

{
  "externalReference": "EXT-1",
  "priority": "URGENT"
}

Bad model:

public record Case(
        String externalReference,
        Priority priority
) {
}

Kenapa buruk?

  • tidak membedakan requested priority dan final priority;
  • domain sulit audit;
  • response bisa membingungkan;
  • event tidak menjelaskan apakah priority berasal dari client atau system.

Better model:

public record OpenCaseCommand(
        ExternalReference externalReference,
        RequestedPriority requestedPriority
) {
}
public final class Case {
    private Priority finalPriority;
    private RequestedPriority requestedPriority;

    public static Case open(OpenCaseCommand command, PriorityPolicy policy) {
        Priority finalPriority = policy.resolve(command.requestedPriority());
        return new Case(command.requestedPriority(), finalPriority);
    }
}

Response:

public record CaseIntakeResponse(
        String caseId,
        String requestedPriority,
        String assignedPriority,
        String status
) {
}

Event:

public record CaseOpenedEventV1(
        String caseId,
        String requestedPriority,
        String assignedPriority
) {
}

Makna menjadi jelas.


25. Mini Case Study: External Reference vs Internal Id

Payload:

{
  "caseId": "CASE-123"
}

Pertanyaan:

  • apakah caseId dari client boleh menentukan id internal?
  • apakah ini sebenarnya externalReference?
  • apakah id internal harus generated?
  • apakah external reference unik per tenant/channel?
  • apakah response perlu mengembalikan keduanya?

Better request:

public record CaseIntakeRequest(
        String externalReference
) {
}

Better response:

public record CaseIntakeResponse(
        String caseId,
        String externalReference
) {
}

Internal command:

public record OpenCaseCommand(
        TenantId tenantId,
        ExternalReference externalReference
) {
}

Internal id:

public record CaseId(String value) {
}

Rule:

Jangan biarkan nama field membuat trust decision tersamar.


26. Mermaid: Complete Boundary Flow

Saat ada bug data, cari di tahap yang benar.

Contoh:

GejalaKemungkinan layer
JSON malformedparser
enum unknowndeserializer
required missingvalidation
priority salah defaultmapper/application
transition illegaldomain
response field bocorresponse mapper/serializer
XML namespace gagalXML parser/binding
duplicate external refapplication/database constraint
consumer event gagalevent contract/evolution

27. Exercise

Desain model untuk requirement berikut.

Requirement

API menerima request create customer:

{
  "externalCustomerId": "C-001",
  "name": "Ayu",
  "email": "ayu@example.com",
  "tier": "GOLD",
  "isVerified": true
}

Aturan:

  • externalCustomerId berasal dari partner;
  • name wajib;
  • email optional tetapi jika ada harus valid;
  • tier boleh dikirim partner sebagai requested tier;
  • final tier ditentukan sistem;
  • isVerified tidak boleh dipercaya dari partner;
  • response harus mengembalikan internal customerId.

Pertanyaan:

  1. Field apa saja di request DTO?
  2. Field apa saja di command?
  3. Field apa saja di domain?
  4. Field apa saja di response?
  5. Field mana yang harus diabaikan/ditolak?
  6. Validasi apa di DTO?
  7. Validasi apa di domain/application?
  8. Mapper apa yang diperlukan?

Jawaban arah:

  • request DTO sebaiknya tidak punya isVerified, atau unknown field policy menolaknya;
  • command punya requestedTier, bukan tier;
  • domain punya assignedTier;
  • response boleh punya customerId, externalCustomerId, assignedTier, status;
  • final verification status berasal dari workflow internal;
  • email validation bisa di DTO;
  • tier policy di domain/application;
  • mapper memisahkan requested vs assigned.

28. Checklist Penguasaan Part 002

Kamu dianggap menguasai part ini jika bisa:

  • membedakan DTO, command, domain model, entity, event, read model, dan error model;
  • menjelaskan mengapa entity tidak boleh langsung menjadi API contract;
  • menjelaskan mengapa event contract lebih sulit diubah daripada response;
  • menentukan lokasi mapper berdasarkan boundary;
  • menentukan lokasi validation berdasarkan jenis rule;
  • menjelaskan trust boundary;
  • mendesain model untuk create vs patch;
  • memetakan failure ke parser/deserializer/validator/mapper/domain;
  • membuat boundary review checklist untuk PR.

29. Referensi Resmi dan Primer


30. Ringkasan

Data boundary bukan sekadar tempat object masuk dan keluar. Boundary adalah tempat ownership, trust, lifecycle, compatibility, dan invariant berubah.

Model yang mirip shape-nya bisa punya peran yang berbeda:

  • DTO menjaga contract;
  • command membawa intent;
  • domain menjaga invariant;
  • entity menjaga persistence;
  • event membawa fakta lintas waktu;
  • read model mengoptimalkan konsumsi;
  • error model menjelaskan kegagalan.

Pada Part 003, kita akan masuk ke semantic mapping vs mechanical copying: mengapa mapper yang compile dan terlihat benar tetap bisa menciptakan bug bisnis yang sulit dideteksi.

Lesson Recap

You just completed lesson 02 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.