In pharmaceutical manufacturing, quality control, auditing, and traceability are critical for ensuring the safety and efficacy of medicines. As the industry increasingly adopts Industry 4.0 and IoT-driven manufacturing, Event Sourcing and CQRS become vital patterns for managing production processes, ensuring auditability, and scaling efficiently. Event Sourcing stores every change as an event, providing full traceability. Combined with CQRS, this allows pharmaceutical companies to handle data management, machine coordination, and regulatory compliance in a flexible, scalable architecture.

In this post, we will explore how CQRS and Event Sourcing can be applied to manage critical processes in pharmaceutical manufacturing with Spring Boot, Kafka, and RabbitMQ. This architecture enables capturing events such as machine malfunctions, production line adjustments, and quality control checks as immutable events, ensuring comprehensive auditing and real-time insights into manufacturing.


Event Sourcing in Pharmaceutical Manufacturing

In pharmaceutical manufacturing, production quality, equipment operation, and compliance checks must be logged in detail. Event Sourcing allows each of these changes to be recorded as events, ensuring the entire production lifecycle of a drug batch is auditable. Events such as batch creation, quality control testing, and machine state updates are stored, allowing operators to rebuild the production state if an issue occurs.

Key Benefits:

  • Traceability: Every step in production can be tracked and verified.
  • Audibility: Regulatory authorities can access detailed event logs.
  • Resilience: Fault-tolerant systems that can rebuild production state in case of failure.

Code Example 1: Defining Pharmaceutical Domain Events

Java
public interface ManufacturingEvent {
    String getEventType();
    LocalDateTime getOccurredOn();
}

public class BatchCreatedEvent implements ManufacturingEvent {
    private final UUID batchId;
    private final LocalDateTime occurredOn;

    public BatchCreatedEvent(UUID batchId) {
        this.batchId = batchId;
        this.occurredOn = LocalDateTime.now();
    }

    @Override
    public String getEventType() {
        return "BatchCreated";
    }

    @Override
    public LocalDateTime getOccurredOn() {
        return occurredOn;
    }
}

Here, the BatchCreatedEvent stores details when a new drug batch is started. By implementing a shared interface (ManufacturingEvent), various events can be recorded in a consistent format.

Code Example 2: Storing and Retrieving Events

Java
@Service
public class EventStore {
    private final List<ManufacturingEvent> eventList = new ArrayList<>();

    public void saveEvent(ManufacturingEvent event) {
        eventList.add(event);
    }

    public List<ManufacturingEvent> getEvents(UUID batchId) {
        return eventList.stream()
                        .filter(event -> event instanceof BatchCreatedEvent 
                                      && ((BatchCreatedEvent) event).getBatchId().equals(batchId))
                        .collect(Collectors.toList());
    }
}

In this example, we store pharmaceutical production events in an in-memory list. In real-world applications, Kafka or a database would persist these events for long-term auditing.


CQRS for Pharmaceutical Manufacturing

Command Query Responsibility Segregation (CQRS) separates the Command Model (responsible for state-changing actions) from the Query Model (responsible for retrieving data). This ensures better performance and scalability in manufacturing systems where data writes and reads have different requirements.

For instance, commands may involve operations like starting a new drug batch or logging equipment calibration, while queries handle retrieving the history of quality checks for compliance purposes.

Code Example 3: Creating a Command (Start a Batch)

Java
public class StartBatchCommand {
    private UUID batchId;
    private String drugName;

    public StartBatchCommand(UUID batchId, String drugName) {
        this.batchId = batchId;
        this.drugName = drugName;
    }
}

Code Example 4: Handling Commands (Batch Handler)

Java
@Service
public class BatchCommandHandler {
    @Autowired
    private EventStore eventStore;

    public void handleStartBatch(StartBatchCommand command) {
        BatchCreatedEvent event = new BatchCreatedEvent(command.getBatchId());
        eventStore.saveEvent(event);
    }
}

The BatchCommandHandler listens for commands such as StartBatchCommand and translates them into events that capture the batch creation process.

Code Example 5: Querying the Read Model (Batch History)

Java
public class BatchView {
    private UUID batchId;
    private String drugName;
    private List<String> qualityChecks;
    // Getters and Setters omitted for brevity
}

The query model can retrieve a drug batch’s entire history, ensuring auditors and regulators can access a full view of all production steps.


Event Handling with Kafka and RabbitMQ in Manufacturing

In pharmaceutical manufacturing, events such as machine breakdowns, batch starts, or quality test results must be processed in real-time to maintain operational efficiency and ensure regulatory compliance. Kafka provides a robust solution for distributed event streaming, while RabbitMQ offers flexible message routing between systems like production lines, quality control, and monitoring services.

Code Example 6: Publishing Events to Kafka

Java
@Service
public class KafkaEventPublisher {
    @Autowired
    private KafkaTemplate<String, ManufacturingEvent> kafkaTemplate;

    public void publish(String topic, ManufacturingEvent event) {
        kafkaTemplate.send(topic, event);
    }
}

Here, each manufacturing event is published to Kafka, allowing for real-time monitoring and reaction to events across different parts of the factory floor.

Code Example 7: Consuming Events from Kafka (Machine Monitoring)

Java
@KafkaListener(topics = "manufacturing-events", groupId = "machine-monitor")
public void listenForBatchCreated(BatchCreatedEvent event) {
    System.out.println("New batch created: " + event.getBatchId());
    // React to new batch events, trigger machine adjustments if needed
}

This listener reacts to events such as batch creation, allowing automated machine systems to adjust or alert operators.

Code Example 8: RabbitMQ for Process Control

Java
@Service
public class RabbitMQEventPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String exchange, String routingKey, ManufacturingEvent event) {
        rabbitTemplate.convertAndSend(exchange, routingKey, event);
    }
}

Code Example 9: RabbitMQ Consumer for Quality Control Alerts

Java
@RabbitListener(queues = "quality-control")
public void handleQualityCheck(QualityCheckPassedEvent event) {
    System.out.println("Quality check passed for batch: " + event.getBatchId());
}

RabbitMQ enables more flexible routing and queuing, particularly useful for handling quality control processes or urgent alerts when deviations occur.


Real-Time Projections with Kafka Streams

To track the state of drug batches or machine health in real-time, Kafka Streams allows projecting the events into read-optimized models for fast querying by operators or regulatory systems.

Code Example 10: Kafka Streams for Live Projections

Java
@Bean
public KStream<String, BatchCreatedEvent> kStream(StreamsBuilder builder) {
    KStream<String, BatchCreatedEvent> stream = builder.stream("manufacturing-events");

    stream.foreach((key, event) -> {
        System.out.println("Updating live batch projection: " + event.getBatchId());
        // Update a projection model (e.g., current state of each batch)
    });
    return stream;
}

This allows pharmaceutical companies to track every batch in production, monitor its current state, and trigger alerts or reports when necessary.


Handling Eventual Consistency in Manufacturing

In a distributed manufacturing environment, ensuring consistency across services (e.g., production line machines, quality control, and inventory) can be challenging. Eventual consistency allows the system to handle some latency, where the write side (production events) might take a few moments to reflect on the read side (current batch status).

Code Example 11: Ensuring Eventual Consistency

Java
@Service
public class ConsistencyService {
    @Autowired
    private EventStore eventStore;
    @Autowired
    private KafkaEventPublisher kafkaEventPublisher;

    public void handleStartBatch(StartBatchCommand command) {
        BatchCreatedEvent event = new BatchCreatedEvent(command.getBatchId());
        eventStore.saveEvent(event);
        kafkaEventPublisher.publish("manufacturing-events", event);
    }

    @KafkaListener(topics = "manufacturing-events", groupId = "projection-service")
    public void updateBatchProjection(BatchCreatedEvent event) {
        System.out.println("Updating projection for batch: " + event.getBatchId());
    }
}

Eventual consistency in pharmaceutical manufacturing ensures that production lines, quality control, and reporting systems eventually converge to a consistent state, even if there’s a slight delay between operations.

Event Sourcing and CQRS are ideal patterns for pharmaceutical manufacturing, providing traceability, auditability, and scalability that are critical in such a highly regulated industry. By separating command and query models, we can optimize production line events, machine coordination, and quality control checks independently, improving system resilience and scalability.

Leveraging Kafka and RabbitMQ ensures robust, real-time event handling across manufacturing systems, ensuring that machines, operators, and auditors have the data they need to maintain safe and compliant production processes. With these patterns, pharmaceutical companies can manage vast amounts of production data, ensure high-quality drug manufacturing, and satisfy regulatory requirements with a future-proof, scalable system.