The name directly comes from the fact that event sourcing events are the source of truth. So all of the other data and other data structures are just derived from the events. So we can erase in theory all of those other storages as long as we keep event lock then we can always regenerate them. Event sourcing contains a ordered of our operation so if we have look on the shopping cart.
- At first we are initializing the shopping cart.
- We are adding new product.
- We may remove the product because we decided that we did it by mistake.
- Then We added a new product.
- At the end we are confirming card.
Nice thing about Event sourcing is that we are able to do time traveling. If we have recorded the sequence of events then we can always go back, So we can just take the events and apply that to the current state and get back to time to see what has happened.
Use case :
Let’s take a use case. Gaurav is a shop keeper, he sells electronic items like mobile phones, laptops etc, he wants to keep track of stock in his shop and wants to know whether his shop has stock of a particular item or not without checking manually. He wants an app for it.
The app has three functionalities:
- User can add new stock.
- He can remove stock after selling it.
- User can find the current stock of a particular item.
In Event Sourcing you just capture user events and add them in database, you just keep adding new events for every user action and no record is updated or deleted in the database , just events are added. With events, you also add event data specific to the event.
In this way you maintain the history of the user action. It is useful if your application has security requirements to audit all user actions. This is also useful in any application where you want a history of user actions (eg Github commits, analytics applications, etc.) and to know the current state of an entity, you simply iterate through your code. are and receive it.
The project structure will be as follows-
The pom.xml will be as follows-
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>stockmanagement_eventstore</artifactId> <version>1.0.0</version> <name>stockmanagement_eventstore</name> <description>Demo project for Event Sourcing</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- H2 database dependency(in-memory databases ) --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Lombok remove boilerplate codes --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Created Stock Model class
package com.example.stock.management; import lombok.Data; //entity model @Data public class Stock { private String name; private int quantity; private String user; }
EventStore class will be as follows
package com.example.stock.management; import java.time.LocalDateTime; import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import lombok.Builder; import lombok.Data; @Entity @Data public class EventStore { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long eventId; private String eventType; private String entityId; private String eventData; private LocalDateTime eventTime; }
Created StockEvent interface
package com.example.stock.management; public interface StockEvent { }
Here is StockAddedEvent class and it’s implementaion
package com.example.stock.management; import lombok.Builder; import lombok.Data; @Builder @Data public class StockAddedEvent implements StockEvent { private Stock stockDetails; }
Created StockRemovedEvent class and it’s implementaion
package com.example.stock.management; import lombok.Builder; import lombok.Data; @Builder @Data public class StockRemovedEvent implements StockEvent { private Stock stockDetails; }
Added EventRepository class
package com.example.stock.management; import java.time.LocalDateTime; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @Component public interface EventRepository extends CrudRepository<EventStore, Long>{ Iterable<EventStore> findByEntityId(String entityId); Iterable<EventStore> findByEntityIdAndEventTimeLessThanEqual(String entityId,LocalDateTime date); }
Created EventService class
package com.example.stock.management; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @Service public class EventService { @Autowired private EventRepository repo; public void addEvent(StockAddedEvent evnt) throws JsonProcessingException { EventStore eventStore = new EventStore(); eventStore.setEventData(new ObjectMapper().writeValueAsString(event.getStockDetails())); eventStore.setEventType("STOCK_ADDED"); eventStore.setEntityId(event.getStockDetails().getName()); eventStore.setEventTime(LocalDateTime.now()); repo.save(eventStore); } public void addEvent(StockRemovedEvent event) throws JsonProcessingException { EventStore eventStore = new EventStore(); eventStore.setEventData(new ObjectMapper().writeValueAsString(event.getStockDetails())); eventStore.setEventType("STOCK_REMOVED"); eventStore.setEntityId(event.getStockDetails().getName()); eventStore.setEventTime(LocalDateTime.now()); repo.save(eventStore); } public Iterable<EventStore> fetchAllEvents(String name) { return repo.findByEntityId(name); } public Iterable<EventStore> fetchAllEventsTillDate(String name,LocalDateTime date) { return repo.findByEntityIdAndEventTimeLessThanEqual(name, date); } }
Created StockController class for adding a stock item , removing a stock item and Getting current count of stock.
package com.example.stock.management; import java.time.LocalDate; import java.time.LocalDateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.gson.Gson; @RestController public class StockController { @Autowired private EventService service; // Adding a stock item @PostMapping("/stock") public void addStock(@RequestBody Stock stockRequest) throws JsonProcessingException { StockAddedEvent event = StockAddedEvent.builder().stockDetails(stockRequest).build(); service.addEvent(event); } // To remove item from a stock @DeleteMapping("/stock") public void removeStock(@RequestBody Stock stock) throws JsonProcessingException { StockRemovedEvent event = StockRemovedEvent.builder().stockDetails(stock).build(); service.addEvent(event); } //To get current count of stock @GetMapping("/stock") public Stock getStock(@RequestParam("name") String name) throws JsonProcessingException { Iterable<EventStore> events = service.fetchAllEvents(name); Stock currentStock = new Stock(); currentStock.setName(name); currentStock.setUser("NA"); for (EventStore event : events) { Stock stock = new Gson().fromJson(event.getEventData(), Stock.class); if (event.getEventType().equals("STOCK_ADDED")) { currentStock.setQuantity(currentStock.getQuantity() + stock.getQuantity()); } else if (event.getEventType().equals("STOCK_REMOVED")) { currentStock.setQuantity(currentStock.getQuantity() - stock.getQuantity()); } } return currentStock; } @GetMapping("/events") public Iterable<EventStore> getEvents(@RequestParam("name") String name) throws JsonProcessingException { Iterable<EventStore> events = service.fetchAllEvents(name); return events; } @GetMapping("/stock/history") public Stock getStockUntilDate(@RequestParam("date") String date,@RequestParam("name") String name) throws JsonProcessingException { String[] dateArray = date.split("-"); LocalDateTime dateTill = LocalDate.of(Integer.parseInt(dateArray[0]), Integer.parseInt(dateArray[1]), Integer.parseInt(dateArray[2])).atTime(23, 59); Iterable<EventStore> events = service.fetchAllEventsTillDate(name,dateTill); Stock currentStock = new Stock(); currentStock.setName(name); currentStock.setUser("NA"); for (EventStore event : events) { Stock stock = new Gson().fromJson(event.getEventData(), Stock.class); if (event.getEventType().equals("STOCK_ADDED")) { currentStock.setQuantity(currentStock.getQuantity() + stock.getQuantity()); } else if (event.getEventType().equals("STOCK_REMOVED")) { currentStock.setQuantity(currentStock.getQuantity() - stock.getQuantity()); } } return currentStock; } }
StockmanagementEventstoreApplication class will be as follows
package com.example.stock.management; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Main class of the application @SpringBootApplication public class StockmanagementEventstoreApplication { public static void main(String[] args) { SpringApplication.run(StockmanagementEventstoreApplication.class, args); } }
Added application.yml file
spring: datasource: url: jdbc:h2:mem:testdb driverClassName: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect h2: console: enabled: true path: /h2
Work Flow
Start the StockmanagementEventstoreApplication app
Adding some items to stock:
Let’s check the database :
We are able to get the current stock by hitting the GET API
If we wants to know what was the stock the day before.
Conclusion
Fetching the current state of an entity is not straightforward and is not scalable in event sourcing. This can be mitigated by taking snapshots of events at a particular time, compute the state of the entity for that snapshot at that time, store it somewhere and then only replay events that occurred after that snapshot time. For more, you can refer to the documentation: https://www.baeldung.com/cqrs-event-sourcing-java