Skip to content

Mappers and DTOs

This guide explains how to work with mappers and DTOs in The Pipeline Framework, following the patterns demonstrated in the CSV Payments reference implementation.

Overview

The Pipeline Framework uses MapStruct-based mappers to convert between different object types during pipeline processing:

  • Domain Objects: The main business objects used within your services
  • DTOs: Data Transfer Objects for internal service processing
  • gRPC Objects: Generated gRPC message types for communication

MapStruct-Based Mappers

Create unified mappers using MapStruct that handle all conversions:

java
@Mapper(
    componentModel = "jakarta",
    uses = {CommonConverters.class},
    unmappedTargetPolicy = ReportingPolicy.WARN
)
public interface PaymentRecordMapper extends Mapper<PaymentRecordGrpc, PaymentRecordDto, PaymentRecord> {

    PaymentRecordMapper INSTANCE = Mappers.getMapper(PaymentRecordMapper.class);

    // Domain ↔ DTO
    @Override
    PaymentRecordDto toDto(PaymentRecord domain);

    @Override
    PaymentRecord fromDto(PaymentRecordDto dto);

    // DTO ↔ gRPC
    @Override
    @Mapping(target = "id", qualifiedByName = "uuidToString")
    @Mapping(target = "amount", qualifiedByName = "bigDecimalToString")
    @Mapping(target = "currency", qualifiedByName = "currencyToString")
    PaymentRecordGrpc toGrpc(PaymentRecordDto dto);

    @Override
    @Mapping(target = "id", qualifiedByName = "stringToUUID")
    @Mapping(target = "amount", qualifiedByName = "stringToBigDecimal")
    @Mapping(target = "currency", qualifiedByName = "stringToCurrency")
    PaymentRecordDto fromGrpc(PaymentRecordGrpc grpc);
}

The same unified mapper interface handles both inbound and outbound conversions, so no separate outbound mapper is needed when using the MapStruct approach.

Working with DTOs

When your service needs to work with DTOs internally, use the main mapper to convert between domain and DTO objects:

java
@PipelineStep(
    // ... annotation configuration
)
@ApplicationScoped
public class ProcessPaymentService implements ReactiveService<PaymentRecord, PaymentStatus> {

    @Inject
    PaymentRecordMapper paymentRecordMapper;  // Main mapper for DTO conversions

    @Override
    public Uni<PaymentStatus> process(PaymentRecord paymentRecord) {
        // Convert domain to DTO for internal processing
        PaymentRecordDto dto = paymentRecordMapper.toDto(paymentRecord);

        // Process the DTO
        PaymentStatusDto statusDto = processPaymentDto(dto);

        // Convert DTO back to domain
        return Uni.createFrom().item(paymentRecordMapper.fromDto(statusDto));
    }

    private PaymentStatusDto processPaymentDto(PaymentRecordDto dto) {
        // Process the DTO and return a status DTO
        return PaymentStatusDto.builder()
            .paymentRecordId(dto.getId())
            .status("PROCESSED")
            .message("Payment record processed successfully")
            .build();
    }
}

Common Converters

The framework provides common converters for standard types that MapStruct cannot handle automatically:

  • UUIDString conversion
  • BigDecimalString conversion
  • CurrencyString conversion
  • LocalDateTimeString conversion
  • List<String>String conversion with delimiters

Best Practices

  1. Use Unified Interface: Create mappers that implement the unified Mapper<Grpc, Dto, Domain> interface
  2. Leverage Common Converters: Use the provided CommonConverters for standard type mappings
  3. Clear Error Handling: Add appropriate error handling for mapping failures
  4. Performance Considerations: Be mindful of expensive mapping operations and cache where appropriate
  5. Validation: Consider validating DTOs after mapping to ensure data integrity