Domain-Driven Design and Resilience Patterns in Goal Service
Introduction
In modern microservices architecture, maintaining system resilience while preserving clear domain boundaries is crucial. The Goal Service demonstrates a sophisticated implementation of Domain-Driven Design (DDD) combined with Resilience4j patterns for handling external service dependencies.
Domain-Driven Design Architecture
The Goal Service follows a clean DDD architecture with distinct layers:
Domain Entity
The core domain entity represents financial goals:
@Entity
@Table(name = "goals")
public class Goal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
private boolean completed;
private LocalDateTime createdAt;
// Getters and setters...
}
Repository Layer
The repository provides data access abstraction:
@Repository
public interface GoalRepository extends JpaRepository<Goal, Long> {
}
Service Layer
The service interface defines the domain contract:
public interface GoalService {
Goal createGoal(Goal goal);
Goal getGoalById(Long id);
List<Goal> getAllGoals();
Goal updateGoal(Long id, Goal goal);
void deleteGoal(Long id);
}
Controller Layer
The REST controller exposes the API endpoints:
@RestController
@RequestMapping("/api/goals")
public class GoalController {
// CRUD operations...
}
Resilience4j Circuit Breaker Implementation
The Goal Service integrates with the Budget Service using Resilience4j patterns to handle potential failures gracefully.
Feign Client with Circuit Breaker
The BudgetServiceClient uses Feign with Resilience4j annotations:
// goal-service/.../client/BudgetServiceClient.java
@FeignClient(name = "budget-service", url = "${external.budget-service.url}", fallback = BudgetServiceClientFallback.class)
public interface BudgetServiceClient {
@GetMapping("/api/budgets/{id}")
@CircuitBreaker(name = "budgetService")
@Retry(name = "budgetService")
BudgetResponse getBudgetById(@PathVariable("id") Long id);
}
Notice the fallback is declared at the @FeignClient level (fallback = BudgetServiceClientFallback.class), not on each method. This keeps the interface clean — every method in the client automatically delegates to the corresponding method in the fallback class when the circuit opens.
Fallback Implementation with Redis Caching
The fallback service provides graceful degradation:
// goal-service/.../client/BudgetServiceClientFallback.java
@Component
public class BudgetServiceClientFallback implements BudgetServiceClient {
@Autowired(required = false) // Redis is optional — graceful when absent
private RedisTemplate<String, Object> redisTemplate;
@Override
public BudgetResponse getBudgetById(Long id) {
// FeignClient fallback requires implementing the interface method directly.
// Delegates to the internal fallback logic.
return fallbackGetBudgetById(id, new RuntimeException("Feign fallback triggered"));
}
public BudgetResponse fallbackGetBudgetById(Long id, Throwable t) {
logger.warn("Fallback triggered for getBudgetById, id: {}, error: {}", id, t.getMessage());
// Try Redis cache first
if (redisTemplate != null) {
try {
BudgetResponse cached = (BudgetResponse) redisTemplate.opsForValue().get("budget:" + id);
if (cached != null) {
logger.info("Returning cached budget from Redis for id: {}", id);
return cached;
}
} catch (Exception e) {
logger.error("Error accessing Redis cache: {}", e.getMessage());
}
}
// Return a sensible default
logger.info("Returning default budget for id: {}", id);
BudgetResponse defaultBudget = new BudgetResponse();
defaultBudget.setId(id);
defaultBudget.setAmount(0.0);
defaultBudget.setCategory("DEFAULT");
return defaultBudget;
}
}
The @Autowired(required = false) on RedisTemplate is a deliberate choice: Redis is an optional dependency. If Redis isn’t running, the fallback still works — it just skips the cache and returns a default response. This avoids a hard dependency on Redis for the circuit breaker to function.
Circuit Breaker Configuration
The circuit breaker configuration in application.yml:
resilience4j:
circuitbreaker:
instances:
budgetService:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 10
retry:
instances:
budgetService:
maxAttempts: 3
waitDuration: 1s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
Circuit Breaker Logic Explained
The circuit breaker pattern prevents cascading failures by monitoring external service calls:
Circuit States
- Closed: Normal operation - requests pass through to the budget service
- Open: Circuit is open - requests are immediately rejected with fallback
- Half-Open: Testing if the external service has recovered
Failure Detection
The circuit breaker tracks:
- Failure Rate Threshold: 50% of calls failing triggers circuit opening
- Sliding Window: Last 10 calls are considered
- Minimum Calls: At least 5 calls before evaluating failure rate
Recovery Mechanism
After waitDurationInOpenState (5 seconds), the circuit transitions to Half-Open state:
- Allows
permittedNumberOfCallsInHalfOpenState(3 calls) to test recovery - If successful, transitions back to Closed
- If failures persist, returns to Open state
Retry Logic
The retry mechanism provides additional resilience:
- Max Attempts: 3 attempts total
- Wait Duration: 1 second between attempts
- Exponential Backoff: Wait time increases exponentially (1s, 2s, 4s)
- Retry Exceptions: Only retries on HTTP server errors and timeouts
Integration Testing
The service includes comprehensive integration tests:
@SpringBootTest
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test")
class BudgetServiceClientIntegrationTest {
@Test
void getBudgetById_ShouldTriggerFallback_WhenExternalServiceIsDown() {
// Arrange
Long budgetId = 1L;
stubFor(get(urlEqualTo("/api/budgets/1"))
.willReturn(aResponse()
.withStatus(HttpStatus.INTERNAL_SERVER_ERROR.value())));
// Act
BudgetResponse response = budgetServiceWrapper.getBudgetById(budgetId);
// Assert
assertNotNull(response);
assertEquals(budgetId, response.getId());
assertEquals(0.0, response.getAmount());
assertEquals("DEFAULT", response.getCategory());
}
}
Monitoring and Observability
The service exposes metrics for monitoring circuit breaker state:
management:
endpoints:
web:
exposure:
include: health,info,prometheus,circuitbreakers
metrics:
tags:
application: goal-service
health:
circuitbreakers:
enabled: true
Best Practices Demonstrated
- Separation of Concerns: Clear DDD layers with distinct responsibilities
- Graceful Degradation: Fallback mechanisms when external services fail
- Circuit Breaker Pattern: Prevents cascading failures
- Retry with Backoff: Intelligent retry logic with exponential backoff
- Caching Strategy: Redis caching for fallback data
- Comprehensive Testing: Integration tests with WireMock
- Configuration Management: Externalized configuration for different environments
Conclusion
The Goal Service demonstrates a production-ready implementation of DDD and resilience patterns. The circuit breaker pattern ensures system stability even when external dependencies fail, while the DDD architecture maintains clean domain boundaries and testability.
This approach provides a solid foundation for building robust microservices that can handle real-world failure scenarios while maintaining clear domain logic separation.