Said OlanoCombining DDD with API governance and integration patterns to build scalable, maintainable distributed systems in Java with practical examples.
Microservices architectures promise scalability and team autonomy. Yet, many organizations discover that distributed systems without proper domain alignment and API governance become chaotic integration nightmares.
The solution? Combining three powerful concepts:
This guide provides practical, production-ready strategies for architects and senior engineers building resilient distributed systems in Java.
Teams build services around databases, not business domains. They create:
UserService, OrderService, PaymentService (tech-driven)CustomerAccountBoundedContext, OrderFulfillmentBoundedContext (business-driven)Result: Unclear ownership, duplicated logic, version mismatches.
APIs evolve without governance:
Services communicate inconsistently:
DDD teaches us to organize code around business domains, not technology.
// ❌ BAD: Tech-driven service
@Service
public class UserService {
public User createUser(UserDTO dto) { ... }
public User updateUser(UserDTO dto) { ... }
}
// ✅ GOOD: Domain-driven service
@Service
public class CustomerAccountService {
private final CustomerAggregateRepository repository;
private final DomainEventPublisher eventPublisher;
public CustomerId registerNewCustomer(
CustomerEmail email,
CompanyName company) {
// Business logic, not CRUD
Customer customer = Customer.register(email, company);
// Side effect: publish domain event
eventPublisher.publish(
new CustomerRegisteredEvent(
customer.getId(),
customer.getEmail()
)
);
repository.save(customer);
return customer.getId();
}
public void updateBillingAddress(
CustomerId id,
BillingAddress address) {
Customer customer = repository.findById(id)
.orElseThrow(() -> new CustomerNotFound(id));
customer.updateBillingAddress(address);
eventPublisher.publish(
new BillingAddressUpdatedEvent(id, address)
);
repository.save(customer);
}
}
Why this matters:
API governance is not about bureaucracy—it is about reliability.
@RestController
@RequestMapping("/api/v2/customers")
@ApiVersion("2.0")
public class CustomerApiController {
private final CustomerAccountService service;
@PostMapping
@ApiOperation("Register a new customer")
@ApiResponse(code = 201, message = "Customer created")
@ApiResponse(code = 400, message = "Invalid input")
@ApiResponse(code = 409, message = "Email already registered")
public ResponseEntity<CustomerResponse> registerCustomer(
@Valid @RequestBody RegisterCustomerRequest request) {
try {
CustomerId id = service.registerNewCustomer(
new CustomerEmail(request.getEmail()),
new CompanyName(request.getCompany())
);
return ResponseEntity
.created(URI.create("/api/v2/customers/" + id.value()))
.body(CustomerResponse.of(id));
} catch (EmailAlreadyRegisteredException e) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(CustomerResponse.error("Email already registered"));
}
}
}
@RestController
@RequestMapping("/api/v2/orders")
public class OrderApiV2 {
// Current implementation
}
@RestController
@RequestMapping("/api/v1/orders")
@Deprecated(since = "2023-06-01", forRemoval = true)
public class OrderApiV1 {
// Legacy - scheduled for removal
}
@Component
public class ApiSlaMonitoring {
@Around("@annotation(ApiSla)")
public Object enforceApiSla(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - startTime;
ApiSla sla = getMethodAnnotation(pjp, ApiSla.class);
if (duration > sla.maxResponseTimeMs()) {
logger.warn("SLA violation: {} took {}ms (max: {}ms)",
pjp.getSignature(), duration, sla.maxResponseTimeMs());
}
return result;
} catch (Exception e) {
recordApiError(pjp, e);
throw e;
}
}
}
@Service
public class OrderService {
private final OrderRepository repository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public OrderId createOrder(CreateOrderCommand cmd) {
Order order = Order.create(
cmd.getCustomerId(),
cmd.getLineItems(),
cmd.getShippingAddress()
);
repository.save(order);
eventPublisher.publishEvent(
new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount()
)
);
return order.getId();
}
}
@Service
public class OrderSagaOrchestrator {
private final OrderService orderService;
private final PaymentService paymentService;
private final InventoryService inventoryService;
@Transactional
public void processOrder(CreateOrderCommand cmd) {
OrderId orderId = orderService.createOrder(cmd);
try {
inventoryService.reserveItems(orderId, cmd.getLineItems());
paymentService.charge(orderId, cmd.getPaymentMethod(), cmd.getTotalAmount());
} catch (PaymentFailedException e) {
inventoryService.releaseItems(orderId);
orderService.markOrderAsFailed(orderId);
throw e;
}
}
}
@Service
public class ResilientPaymentClient {
@CircuitBreaker(name = "paymentService", failureThreshold = 5, delay = 1000)
@Retry(maxAttempts = 3)
@Timeout(value = 2000)
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject(
PAYMENT_SERVICE_URL + "/payments",
request,
PaymentResponse.class
);
}
}
Building scalable distributed systems requires:
Start with clear domain boundaries, define contracts upfront, and choose integration patterns based on consistency requirements.