# Inleiding: De Uitdaging van Moderne Dataarchitectuur
In de wereld van microservices en gedistribueerde systemen worden we steeds vaker geconfronteerd met een fundamentele uitdaging: hoe schalen we onze applicaties efficiënt terwijl we zowel lees- als schrijfoperaties optimaal bedienen? De traditionele aanpak waarbij één datamodel beide verantwoordelijkheden draagt, leidt vaak tot prestatieproblemen en complexe codebases.
Na onze verkenningen van het Saga-patroon, Event Sourcing en de GRASP-richtlijnen in voorgaande blogs, is het tijd om een complementair patroon te onderzoeken: Command Query Responsibility Segregation (CQRS). Dit patroon vormt samen met Event Sourcing een krachtige combinatie die de backbone kan zijn van moderne, schaalbare applicaties.
# Wat is CQRS?
CQRS is gebaseerd op een eenvoudig maar krachtig principe: splits je datamodel in twee delen:
- Command-model: Geoptimaliseerd voor schrijfoperaties (commands)
- Query-model: Geoptimaliseerd voor leesoperaties (queries)
Dit in tegenstelling tot het traditionele CRUD-model, waarbij één model verantwoordelijk is voor zowel lezen als schrijven:
┌─────────────────────┐
│ │
│ Traditioneel │
│ CRUD Model │
│ │
└─────────────────────┘
▲ ▲
│ │
│ │
┌──────┘ └──────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│Commands │ │ Queries │
└─────────┘ └─────────┘
Versus het CQRS-model:
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ Command Model │ │ Query Model │
│ │ │ │
└─────────────────┘ └─────────────────┘
▲ ▲
│ │
│ │
│ │
┌──────┘ ┌─────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│Commands │ │ Queries │
└─────────┘ └─────────┘
# Waarom CQRS Implementeren?
Er zijn verschillende situaties waarin CQRS een uitstekende oplossing biedt:
- Asymmetrische schaalbaarheid: Leesoperaties (queries) komen vaak veel vaker voor dan schrijfoperaties (commands).
- Complexe domeinen: Bij complexe business logica kan het handig zijn om het schrijfmodel te optimaliseren voor consistentie en het leesmodel voor presentatie.
- Rapportage behoeften: Vaak heb je voor rapportages gedenormaliseerde data nodig, terwijl je voor transacties genormaliseerde data wilt.
- Integratie met Event Sourcing: CQRS werkt naadloos samen met Event Sourcing, wat we eerder hebben besproken.
# CQRS in de Praktijk: Een Java-implementatie
Laten we een praktische implementatie bekijken van CQRS in een Java-applicatie. We bouwen voort op onze eerdere discussie over microservices en gebruiken Spring Boot.
# 1. De Basisstructuur
// Command model - geoptimaliseerd voor schrijven
@Entity
public class ProductCommand {
@Id
private String productId;
private String name;
private BigDecimal price;
private int stockQuantity;
// Validatielogica en domeinregels hier
}
// Query model - geoptimaliseerd voor lezen
@Entity
@Table(name = "product_view")
public class ProductQuery {
@Id
private String productId;
private String name;
private BigDecimal price;
private int stockQuantity;
private String category;
private List<String> tags;
private int reviewCount;
private BigDecimal averageRating;
// Geen domeinlogica, pure data
}
# 2. De Command-zijde
@Service
public class ProductCommandService {
private final ProductCommandRepository repository;
private final EventPublisher eventPublisher;
@Transactional
public void createProduct(CreateProductCommand command) {
// Validatie
validateProductCommand(command);
// Creëer het product
ProductCommand product = new ProductCommand(
UUID.randomUUID().toString(),
command.getName(),
command.getPrice(),
command.getStockQuantity()
);
// Persisteer
repository.save(product);
// Publiceer event voor synchronisatie
eventPublisher.publish(new ProductCreatedEvent(product));
}
@Transactional
public void updateStock(UpdateStockCommand command) {
ProductCommand product = repository.findById(command.getProductId())
.orElseThrow(() -> new ProductNotFoundException(command.getProductId()));
// Business regels toepassen
if (command.getNewQuantity() < 0) {
throw new InvalidStockQuantityException();
}
product.setStockQuantity(command.getNewQuantity());
repository.save(product);
// Publiceer event voor synchronisatie
eventPublisher.publish(new ProductStockUpdatedEvent(product));
}
}
# 3. De Query-zijde
@Service
public class ProductQueryService {
private final ProductQueryRepository repository;
public ProductQueryDTO getProductById(String id) {
return repository.findById(id)
.map(this::mapToDTO)
.orElseThrow(() -> new ProductNotFoundException(id));
}
public List<ProductQueryDTO> findProductsByCategory(String category) {
return repository.findByCategory(category).stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}
public List<ProductQueryDTO> findTopRatedProducts(int limit) {
return repository.findByOrderByAverageRatingDesc(PageRequest.of(0, limit)).stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}
private ProductQueryDTO mapToDTO(ProductQuery product) {
return new ProductQueryDTO(
product.getProductId(),
product.getName(),
product.getPrice(),
product.getStockQuantity(),
product.getCategory(),
product.getTags(),
product.getReviewCount(),
product.getAverageRating()
);
}
}
# 4. Synchronisatie tussen Command en Query modellen
De synchronisatie tussen de command- en query-modellen is een cruciaal onderdeel van CQRS. Hiervoor gebruiken we events:
@Service
public class ProductEventHandler {
private final ProductQueryRepository queryRepository;
@EventListener
public void handleProductCreated(ProductCreatedEvent event) {
ProductQuery productView = new ProductQuery();
productView.setProductId(event.getProductId());
productView.setName(event.getName());
productView.setPrice(event.getPrice());
productView.setStockQuantity(event.getStockQuantity());
productView.setCategory(event.getCategory());
productView.setTags(event.getTags());
productView.setReviewCount(0);
productView.setAverageRating(BigDecimal.ZERO);
queryRepository.save(productView);
}
@EventListener
public void handleStockUpdated(ProductStockUpdatedEvent event) {
queryRepository.findById(event.getProductId())
.ifPresent(product -> {
product.setStockQuantity(event.getNewQuantity());
queryRepository.save(product);
});
}
}
# Architecturale Overwegingen bij CQRS
Bij het implementeren van CQRS zijn er belangrijke architecturale overwegingen:
# 1. Eventual Consistency
Met gescheiden lees- en schrijfmodellen introduceren we eventual consistency: het leesmodel is niet altijd direct gesynchroniseerd met het schrijfmodel. Dit moet je ontwerp hierop aanpassen:
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductCommandService commandService;
private final ProductQueryService queryService;
@PostMapping
public ResponseEntity<String> createProduct(@RequestBody CreateProductRequest request) {
CreateProductCommand command = mapToCommand(request);
commandService.createProduct(command);
// Return 202 Accepted omdat de view mogelijk nog niet is bijgewerkt
return ResponseEntity.accepted().body("Product creation in progress");
}
@GetMapping("/{id}")
public ResponseEntity<ProductQueryDTO> getProduct(@PathVariable String id) {
return ResponseEntity.ok(queryService.getProductById(id));
}
}
# 2. Databasekeuze
Vaak is het zinvol om verschillende databases te gebruiken voor je command- en query-modellen:
- Command-model: Vaak een relationele database (PostgreSQL, MySQL) voor ACID-transacties
- Query-model: Kan een NoSQL-oplossing zijn (MongoDB, Elasticsearch) voor flexibiliteit en prestaties
# 3. Integratie met Event Sourcing
CQRS werkt uitzonderlijk goed samen met Event Sourcing, waarbij alle wijzigingen als events worden opgeslagen:
@Service
public class ProductEventSourcedService {
private final EventStore eventStore;
private final EventPublisher eventPublisher;
@Transactional
public void handleCommand(CreateProductCommand command) {
// Valideer command
validateCommand(command);
// Creëer event
ProductCreatedEvent event = new ProductCreatedEvent(
UUID.randomUUID().toString(),
command.getName(),
command.getPrice(),
command.getStockQuantity()
);
// Sla event op in event store
eventStore.store(event);
// Publiceer voor eventhandlers (om query model bij te werken)
eventPublisher.publish(event);
}
}
# Praktijkvoorbeeld: Een Online Webshop
Laten we de concepten toepassen op een praktisch voorbeeld: een online webshop.
# Het Command-model voor Bestellingen
@Service
public class OrderCommandService {
private final OrderRepository repository;
private final ProductCommandRepository productRepository;
private final EventPublisher eventPublisher;
@Transactional
public String placeOrder(PlaceOrderCommand command) {
// Validatie van beschikbaarheid
validateProductAvailability(command.getItems());
// Creëer order
OrderCommand order = new OrderCommand(
UUID.randomUUID().toString(),
command.getCustomerId(),
command.getItems(),
OrderStatus.PENDING,
LocalDateTime.now()
);
// Update voorraad
updateInventory(command.getItems());
// Persisteer order
repository.save(order);
// Publiceer event
eventPublisher.publish(new OrderPlacedEvent(order));
return order.getOrderId();
}
private void updateInventory(List<OrderItemCommand> items) {
for (OrderItemCommand item : items) {
ProductCommand product = productRepository.findById(item.getProductId())
.orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
int newQuantity = product.getStockQuantity() - item.getQuantity();
if (newQuantity < 0) {
throw new InsufficientStockException(item.getProductId());
}
product.setStockQuantity(newQuantity);
productRepository.save(product);
// Publiceer event voor synchronisatie
eventPublisher.publish(new ProductStockUpdatedEvent(
product.getProductId(), newQuantity));
}
}
}
# Het Query-model voor Bestellingen
@Service
public class OrderQueryService {
private final OrderQueryRepository repository;
public List<OrderSummaryDTO> getCustomerOrders(String customerId) {
return repository.findByCustomerId(customerId).stream()
.map(this::mapToSummaryDTO)
.collect(Collectors.toList());
}
public OrderDetailDTO getOrderDetails(String orderId) {
return repository.findById(orderId)
.map(this::mapToDetailDTO)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// DTOs en mapping methoden...
}
# Prestatievoordelen van CQRS
Een van de grootste voordelen van CQRS is de mogelijkheid om de lees- en schrijfkant onafhankelijk te schalen. Laten we kijken naar enkele prestatieverbeteringen:
- Geoptimaliseerde leesmodellen: Je kunt query-specifieke denormaliseerde modellen maken.
- Caching: Het query-model kan effectiever worden gecached omdat het minder vaak verandert.
- Schaalbaar schrijven: Commands kunnen asynchroon worden verwerkt voor hogere throughput.
# Uitdagingen en Valkuilen
CQRS is niet zonder uitdagingen:
- Complexiteit: Het patroon introduceert extra complexiteit die niet altijd gerechtvaardigd is voor eenvoudige applicaties.
- Eventual Consistency: Gebruikers moeten mogelijk worden opgeleid dat wijzigingen niet altijd onmiddellijk zichtbaar zijn.
- Dubbele gegevensopslag: Je slaat gegevens dubbel op, wat meer opslagruimte vereist.
# Best Practices
Op basis van mijn ervaring met CQRS en Event Sourcing in productie, hier enkele best practices:
- Begin klein: Implementeer CQRS eerst voor één bounded context, niet voor je hele applicatie.
- Gebruik Event Sourcing verstandig: Niet elk deel van je applicatie heeft Event Sourcing nodig.
- Optimaliseer query-modellen: Maak specifieke query-modellen voor verschillende use cases.
- Monitoring: Implementeer monitoring om synchronisatieproblemen snel te detecteren.
- Idempotente event handlers: Zorg ervoor dat je event handlers idempotent zijn voor herhaalde events.
# Conclusie
CQRS is een krachtig patroon dat, wanneer juist toegepast, aanzienlijke voordelen biedt voor complexe, schaalbare applicaties. Het scheiden van lees- en schrijfmodellen geeft je de vrijheid om elke kant te optimaliseren voor zijn specifieke taak, wat leidt tot betere prestaties en schaalbaarheid.
In combinatie met patroonen die we eerder hebben besproken, zoals Saga en Event Sourcing, vormt CQRS een belangrijk onderdeel van de moderne architectuurgereedschapskist voor gedistribueerde systemen.
In een volgende blog zullen we dieper ingaan op de implementatie van CQRS met specifieke technologieën zoals Apache Kafka voor event streaming en Elasticsearch voor geoptimaliseerde query-modellen.
Heb je ervaring met CQRS in je projecten? Deel je ervaringen en uitdagingen in de reacties!
# Referenties
- Fowler, M. (2011). CQRS. https://martinfowler.com/bliki/CQRS.html
- Young, G. (2010). CQRS Documents. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- Vernon, V. (2013). Implementing Domain-Driven Design. Addison-Wesley Professional.
- Richardson, C. (2018). Microservices Patterns. Manning Publications.
Reacties (0 )
Geen reacties beschikbaar.
Log in om een reactie te plaatsen.