Event Sourcing with SpringBoot

Reading Time: 5 minutes

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.

event-sourcing

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-

project-structure

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:

stock-item1

stock-item2

stock-item3

Let’s check the database :

stock-database

We are able to get the current stock by hitting the GET API

get-stock-item-via-name

If we wants to know what was the stock the day before.

get-stock-item-via-date

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

Written by 

Gaurav Dubey is a Java Developer at Knoldus Software LLP. He has done M.CA from Kurukshetra University and completed Bachelor's Degree in Computer Science from M. D. University. He is a tech enthusiast with good knowledge of Java. He is majorly focused in Java practice. On the personal front, he loves to cook and travel.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading