Four Context Mapping Patterns for Cross‑Bounded‑Context Collaboration

The article explains how to integrate multiple bounded contexts in a payroll system by using four DDD‑based relationship patterns—Shared Kernel, Customer‑Supplier, Anti‑Corruption Layer, and Open Host Service—detailing their motivations, code organization, risks, and selection criteria.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Four Context Mapping Patterns for Cross‑Bounded‑Context Collaboration

Why Communication Between Bounded Contexts Matters

The previous post split a payroll system into four bounded contexts: Employee Information, Attendance, Payroll Calculation, and Tax. Each context has its own language and model, but real‑world requirements force them to communicate, e.g., payroll needs attendance data, employee salary grades, and external bank APIs.

Directly importing classes from another context (e.g., AttendanceRecord) leads to compile‑time failures and subtle bugs when the supplier changes method names or semantics, illustrating the cost of missing collaboration contracts.

Context Mapping Is More Than an API Topology

Context Mapping should answer who decides, who compromises, and where the boundaries lie when two bounded contexts need to cooperate. DDD lists eight relationship modes; this article condenses them into four practical patterns that cover 95 % of the payroll system scenarios.

Shared Kernel : a minimal set of common concepts.

Customer‑Supplier : internal teams negotiate an API contract.

Anti‑Corruption Layer (ACL) : isolates dirty external data.

Open Host Service (OHS) : the provider publishes a stable protocol for many downstream consumers.

Posture One – Shared Kernel

Problem it solves

When several contexts need the same identifier or value object (e.g., EmployeeId, Money, PayCycle), duplicating them forces conversion code and risks mismatched definitions.

Code organization

payroll-shared-kernel/
├── src/main/java/com/company/shared/domain/
│   ├── EmployeeId.java   // identifier
│   ├── Money.java        // value object with currency
│   ├── PayCycle.java     // year‑month period
│   └── DomainEvent.java // base event class
public final class EmployeeId {
    private final String value;
    private EmployeeId(String value) { /* validation */ }
    public static EmployeeId of(String value) { return new EmployeeId(value); }
    public String getValue() { return value; }
    // equals, hashCode omitted for brevity
}

Risks & Best practice

The biggest danger is boundary creep: starting with EmployeeId and Money, teams may keep adding DepartmentCode, EmployeeStatus, etc., turning the shared kernel into a full‑blown table. Keep it strictly to identifiers and immutable value objects; any business logic belongs elsewhere.

Posture Two – Customer‑Supplier

Problem it solves

Payroll (customer) needs monthly attendance summaries from the Attendance (supplier) context. Both teams can sit down, agree on required fields and format, and the supplier commits to a stable interface.

Code organization

package com.company.attendance.application;
public interface AttendanceFacade {
    /** Query monthly attendance summary for an employee */
    AttendanceSummary queryMonthlySummary(EmployeeId employeeId, PayCycle cycle);
}

public class AttendanceSummary {
    private EmployeeId employeeId;
    private PayCycle cycle;
    private int scheduledWorkDays;
    private int actualWorkDays;
    private int absentDays;
    private int overtimeHours;
    private List<LeaveRecord> leaves;
    // getters only – immutable DTO
}
package com.company.payroll.application;
@Service
public class SalaryCalculationAppService {
    private final AttendanceFacade attendanceFacade;
    private final SalaryCalculationDomainService calculationService;

    public PaySlip calculateMonthlyPaySlip(EmployeeId employeeId, PayCycle cycle) {
        AttendanceSummary attendance = attendanceFacade.queryMonthlySummary(employeeId, cycle);
        Payee payee = payeeRepository.findByEmployeeId(employeeId);
        return calculationService.calculate(payee, attendance, cycle);
    }
}

The key is that the customer depends only on the AttendanceFacade contract, not on any internal domain classes of the supplier.

Posture Three – Anti‑Corruption Layer

Problem it solves

When a bounded context must call an external system (e.g., a bank API), the external data format is often messy: strings for amounts and dates, magic return codes, and many nullable fields. Directly using these structures pollutes the domain model.

Code organization

// External bank response (dirty data)
public class BankTransferResponse {
    private String retCode;   // "0000" means success
    private String retMsg;
    private String txnRef;
    private String bizFlowNo;
    private String txnAmt;    // e.g. "1234.56"
    private String txnDate;   // e.g. "20241201"
    private String txnTime;   // e.g. "143022"
    private String acctNo;    // masked account number
    // many other nullable fields
}
// ACL implementation in infrastructure layer
@Component
public class BankPaymentGatewayAdapter implements PaymentGateway {
    private final BankApiClient bankApiClient;

    @Override
    public PaymentResult transfer(BankAccount target, Money amount, PaymentReference ref) {
        BankTransferRequest request = buildBankRequest(target, amount, ref);
        BankTransferResponse response = bankApiClient.transfer(request);
        return translateResponse(response);
    }

    private PaymentResult translateResponse(BankTransferResponse response) {
        if (!"0000".equals(response.getRetCode())) {
            return PaymentResult.failure(
                BankErrorCode.of(response.getRetCode()),
                response.getRetMsg()
            );
        }
        return PaymentResult.success(
            BankTransactionId.of(response.getBizFlowNo()),
            Money.ofYuan(new BigDecimal(response.getTxnAmt())),
            LocalDate.parse(response.getTxnDate(), DateTimeFormatter.BASIC_ISO_DATE)
        );
    }
}
// Domain‑level contract (clean language)
public interface PaymentGateway {
    PaymentResult transfer(BankAccount target, Money amount, PaymentReference ref);
}

public class PaymentResult {
    private final boolean success;
    private final BankTransactionId transactionId; // value object
    private final Money amount;                  // value object
    private final LocalDate settleDate;           // value object
    private final String failureReason;
    // constructors, getters omitted
}

When the bank changes, only a new adapter (e.g., NewBankPaymentGatewayAdapter) is needed; the domain layer stays untouched, demonstrating the Dependency Inversion Principle in practice.

Posture Four – Open Host Service

Problem it solves

The Employee Information context is the source of truth for many downstream services. Instead of customizing an interface for each consumer, it publishes a versioned, stable API that all downstream contexts adapt to.

Code organization

@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeOpenHostController {
    @GetMapping("/{id}/payroll-profile")
    public ResponseEntity<EmployeePayrollProfileDTO> getPayrollProfile(@PathVariable String id) {
        Employee employee = employeeRepository.findById(EmployeeId.of(id))
            .orElseThrow(() -> new EmployeeNotFoundException(id));
        return ResponseEntity.ok(EmployeePayrollProfileDTO.from(employee));
    }
}

public class EmployeePayrollProfileDTO {
    private String employeeId;
    private String employmentType; // "REGULAR" / "PROBATION" / "CONTRACT"
    private LocalDate hireDate;
    private String salaryLevel;   // "P5" / "P6" …
    public static EmployeePayrollProfileDTO from(Employee employee) { /* mapping */ }
}
@Component
public class HrEmployeeQueryAdapter implements EmployeeProfileQuery {
    private final HrOpenHostClient hrClient;

    @Override
    public EmployeePayrollInfo queryPayrollInfo(EmployeeId employeeId) {
        EmployeePayrollProfileDTO dto = hrClient.getPayrollProfile(employeeId.getValue());
        return EmployeePayrollInfo.builder()
            .employeeId(employeeId)
            .employmentType(EmploymentType.fromCode(dto.getEmploymentType()))
            .hireDate(dto.getHireDate())
            .salaryLevel(SalaryLevel.fromCode(dto.getSalaryLevel()))
            .build();
    }
}

The adapter performs a lightweight format conversion; the heavy‑weight ACL concerns are unnecessary because both sides share a common publishing language.

How to Choose the Right Posture

Multiple contexts share the same identifier or value object → Shared Kernel

Two internal contexts collaborate and the consumer has bargaining power → Customer‑Supplier

Integration with third‑party or legacy systems with messy contracts → Anti‑Corruption Layer

A context is upstream of many downstream consumers → Open Host Service

In practice, adding an ACL is rarely a waste; even internal APIs benefit from a thin adapter when the two teams use divergent styles. If two contexts truly have no need to integrate, the best solution is to keep them separate.

Conclusion

Context Mapping addresses the relational question of “who is stronger, who compromises, and where the boundary lies” rather than merely the technical design of an API. The four patterns—Shared Kernel, Customer‑Supplier, Anti‑Corruption Layer, and Open Host Service—provide a practical toolbox for clean bounded‑context integration.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Anti-Corruption LayerDDDBounded ContextContext MappingShared KernelCustomer SupplierOpen Host Service
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.