Table of Contents

  1. Prerequisites for Spring Batch Multi-Step Job
  2. Deep Dive into Spring Batch Multi-Step Job Concept
  3. Step-by-Step Guide to Creating a Spring Batch Multi-Step Job
  4. Full Example of a Spring Batch Multi-Step Job with Conditional Flow
  5. Common Mistakes to Avoid in Spring Batch Multi-Step Job
  6. Mistake 1: Incorrect Step Configuration
  7. Mistake 2: Insufficient Error Handling
  8. Production Tips for Spring Batch Multi-Step Job
  9. Testing Spring Batch Multi-Step Job with Conditional Flow
  10. Key Takeaways for Implementing Spring Batch Multi-Step Job
  11. Configuring Conditional Flow in Spring Batch Multi-Step Job

Prerequisites for Spring Batch Multi-Step Job

To start with Spring Batch multi-step job with conditional flow, you need to have the required dependencies in your project. The Spring Boot version should be at least 2.5.0, and the Java version should be at least 11. You also need to have the Spring Batch dependency in your project.

The spring-boot-starter-batch dependency includes all the necessary dependencies for Spring Batch. You can add this dependency to your pom.xml file if you are using Maven, or to your build.gradle file if you are using Gradle. For more information on Spring Boot project structure, you can refer to our previous article.

Here is an example of how you can add the spring-boot-starter-batch dependency to your pom.xml file:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

You also need to have a database to store the batch job metadata. You can use any database that is supported by Spring Batch, such as MySQL or PostgreSQL.

Here is an example of a simple BatchConfig class that configures the Spring Batch job:

package com.example.batch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfig {
 
 @Autowired
 private JobBuilderFactory jobBuilderFactory;
 
 @Autowired
 private StepBuilderFactory stepBuilderFactory;
 
 // Define a job that has one step
 @Bean
 public Job job() {
 // Create a new job with a unique id
 return jobBuilderFactory.get("job")
 .incrementer(new RunIdIncrementer())
 .start(step())
 .build();
 }
 
 // Define a step that does something
 @Bean
 public Step step() {
 // Create a new step that does something
 return stepBuilderFactory.get("step")
 .tasklet((contribution, chunkContext) -> {
 // Do something here, for example, log a message
 System.out.println("Doing something...");
 return RepeatStatus.FINISHED;
 })
 .build();
 }
}

When you run this job, you should see the message “Doing something…” printed to the console. The expected output is:

Doing something...

For more information on Spring Batch tutorial, you can refer to our previous article.

Deep Dive into Spring Batch Multi-Step Job Concept

A **job** in Spring Batch is a sequence of **steps**, where each **step** represents a single unit of work. The **flow** of a job determines the order in which these **steps** are executed. To configure a **job**, you need to define a **job** bean and specify the **steps** it will execute. For more information on **job** configuration, refer to our article on Configuring a Spring Batch Job.

The **flow** of a **job** can be controlled using **decision** elements, which allow you to execute different **steps** based on certain conditions. This is useful when you need to handle different scenarios or error conditions. A **decision** element is typically used in conjunction with a **step** to determine the next course of action.

To illustrate this concept, consider a **job** that reads data from a file, processes it, and writes it to a database. If the file is empty, you may want to skip the processing **step** and proceed directly to the next **step**. You can achieve this by using a **decision** element to check the file size and execute the processing **step** only if it is not empty.

Here is an example of how you can configure a **job** with a conditional **flow**:

package com.example.springbatch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class JobConfig {
 @Autowired
 private JobBuilderFactory jobBuilderFactory;
 
 @Autowired
 private StepBuilderFactory stepBuilderFactory;
 
 @Bean
 public Job job() {
 return jobBuilderFactory.get("job")
 .start(step1())
 .next(decider())
 .from(decider()).on("PROCEED").to(step2())
 .from(decider()).on("SKIP").to(step3())
 .end()
 .build();
 }
 
 @Bean
 public Step step1() {
 return stepBuilderFactory.get("step1")
 .tasklet((contribution, chunkContext) -> {
 // Read data from file
 return RepeatStatus.FINISHED;
 })
 .build();
 }
 
 @Bean
 public Step step2() {
 return stepBuilderFactory.get("step2")
 .tasklet((contribution, chunkContext) -> {
 // Process data
 return RepeatStatus.FINISHED;
 })
 .build();
 }
 
 @Bean
 public Step step3() {
 return stepBuilderFactory.get("step3")
 .tasklet((contribution, chunkContext) -> {
 // Write data to database
 return RepeatStatus.FINISHED;
 })
 .build();
 }
 
 @Bean
 public JobExecutionDecider decider() {
 return new JobExecutionDecider() {
 @Override
 public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
 // Check file size and return "PROCEED" if not empty, "SKIP" otherwise
 return new FlowExecutionStatus("PROCEED");
 }
 };
 }
}

When you run this **job**, it will execute **step1**, then use the **decider** to determine whether to execute **step2** or **step3**. The expected output will depend on the file size and the logic implemented in the **decider**.

Job execution complete

For more information on **step** configuration, refer to our article on Configuring a Spring Batch Step.

Step-by-Step Guide to Creating a Spring Batch Multi-Step Job

To create a Spring Batch multi-step job with conditional flow, you need to define a Job bean that contains multiple Step beans. Each Step can have its own ItemReader, ItemProcessor, and ItemWriter.
The conditional flow is achieved using a JobExecutionDecider that determines the next step based on the previous step’s execution result.
For more information on Job configuration, visit our Spring Batch Job Configuration tutorial.

A Job can have multiple Step beans, each with its own ItemReader, ItemProcessor, and ItemWriter.
The ItemReader reads data from a source, the ItemProcessor processes the data, and the ItemWriter writes the processed data to a destination.
The conditional flow is achieved using a JobExecutionDecider that determines the next step based on the previous step’s execution result.

Here is an example of a Spring Batch multi-step job with conditional flow:

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.FlowExecutor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class MultiStepJobConfig {
 
 @Autowired
 private JobBuilderFactory jobBuilderFactory;
 
 @Autowired
 private StepBuilderFactory stepBuilderFactory;
 
 @Bean
 public Job multiStepJob() {
 return jobBuilderFactory.get("multiStepJob")
 .start(step1())
 .next(decider())
 .from(decider()).on("FAILED").to(step2())
 .from(decider()).on("COMPLETED").to(step3())
 .end()
 .build();
 }
 
 @Bean
 public Step step1() {
 // Why: Define the first step with an ItemReader and ItemWriter
 return stepBuilderFactory.get("step1")
 .chunk(1)
 .reader(itemReader())
 .writer(itemWriter())
 .build();
 }
 
 @Bean
 public ItemReader itemReader() {
 // Why: Create a simple ItemReader that reads from a list
 List items = new ArrayList<>();
 items.add("Item1");
 items.add("Item2");
 return new ListItemReader<>(items);
 }
 
 @Bean
 public ItemWriter itemWriter() {
 // Why: Create a simple ItemWriter that prints the items
 return items -> items.forEach(System.out::println);
 }
 
 @Bean
 public Step step2() {
 // Why: Define the second step with an ItemReader and ItemWriter
 return stepBuilderFactory.get("step2")
 .chunk(1)
 .reader(itemReader())
 .writer(itemWriter())
 .build();
 }
 
 @Bean
 public Step step3() {
 // Why: Define the third step with an ItemReader and ItemWriter
 return stepBuilderFactory.get("step3")
 .chunk(1)
 .reader(itemReader())
 .writer(itemWriter())
 .build();
 }
 
 @Bean
 public JobExecutionDecider decider() {
 // Why: Create a decider that determines the next step based on the previous step's execution result
 return (jobExecution, stepExecution) -> {
 if (stepExecution.getStatus() == BatchStatus.FAILED) {
 return new FlowExecutionStatus("FAILED");
 } else {
 return new FlowExecutionStatus("

Full Example of a Spring Batch Multi-Step Job with Conditional Flow

The Spring Batch framework provides a robust way to handle batch processing with conditional flow. To demonstrate this, we will create a job that reads data from a database, processes it, and then writes it to a file. The job will have multiple steps and use decision elements to control the flow. The first step in our job will be to read data from a database using the JdbcCursorItemReader. We will then process the data using a ItemProcessor and write it to a file using the FlatFileItemWriter. The job will use a decision element to determine which step to execute next based on the outcome of the previous step. For more information on ItemReader and ItemWriter interfaces, visit our Spring Batch ItemReader and ItemWriter tutorial. Here is the complete code example:
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.support.CompositeItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Configuration
@EnableBatchProcessing
public class BatchConfig {
 @Autowired
 private JobBuilderFactory jobBuilderFactory;
 @Autowired
 private StepBuilderFactory stepBuilderFactory;

 @Bean
 public Job job() {
 return jobBuilderFactory.get("job")
 .start(step1())
 .next(decider())
 .from(decider())
 .on("FAILED")
 .to(step2())
 .from(decider())
 .on("COMPLETED")
 .to(step3())
 .end()
 .build();
 }

 @Bean
 public Step step1() {
 // Read data from database
 return stepBuilderFactory.get("step1")
 .chunk(10)
 .reader(reader())
 .processor(processor())
 .writer(writer())
 .build();
 }

 @Bean
 public JdbcCursorItemReader<String> reader() {
 // Configure reader to read from database
 JdbcCursorItemReader<String> reader = new JdbcCursorItemReader<>();
 reader.setDataSource(dataSource());
 reader.setSql("SELECT * FROM table");
 return reader;
 }

 @Bean
 public FlatFileItemWriter<String> writer() {
 // Configure writer to write to file
 FlatFileItemWriter<String> writer = new FlatFileItemWriter<>();
 writer.setResource(new ClassPathResource("output.txt"));
 return writer;
 }

 @Bean
 public JobExecutionDecider decider() {
 // Decide which step to execute next
 return new MyDecider();
 }
}

The expected output of this job will be:

Data written to output.txt file

For further reading on Spring Batch and its features, visit our Spring Batch Tutorial and Spring Batch Configuration pages.

Common Mistakes to Avoid in Spring Batch Multi-Step Job

When designing a Spring Batch multi-step job, it is crucial to understand the conditional flow and how to handle potential errors. A common pitfall is not properly configuring the job repository, which can lead to issues with job execution and restartability. For more information on configuring the job repository, see our article on Configuring the Job Repository in Spring Batch.

Mistake 1: Incorrect Step Configuration

A common mistake is incorrectly configuring the Step beans, which can lead to job failures. The following code example demonstrates an incorrect configuration:

public class IncorrectStepConfig {
 // WRONG: missing @Bean annotation
 public Step incorrectStep() {
 // ...
 }
}

This will result in a NoBeanDefinitionFoundException exception. The correct configuration is:

public class CorrectStepConfig {
 @Bean
 public Step correctStep() {
 // configure the step correctly
 // we need to specify the @Bean annotation to register the step as a Spring bean
 return Steps.get("correctStep")
 .tasklet(new Tasklet() {
 @Override
 public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
 // tasklet implementation
 return RepeatStatus.FINISHED;
 }
 })
 .build();
 }
}

Expected output:

Job execution complete

Mistake 2: Insufficient Error Handling

Another common mistake is not implementing sufficient error handling mechanisms, which can lead to job failures and make it difficult to diagnose issues. For more information on error handling in Spring Batch, see our article on Error Handling in Spring Batch. The following code example demonstrates an incorrect implementation:

public class IncorrectErrorHandler {
 // WRONG: not handling potential exceptions
 public void handleException() {
 // ...
 }
}

This will result in an unhandled Exception being thrown. The correct implementation is:

public class CorrectErrorHandler {
 @Bean
 public JobExecutionListener listener() {
 return new JobExecutionListener() {
 @Override
 public void beforeJob(JobExecution jobExecution) {
 // ...
 }
 
 @Override
 public void afterJob(JobExecution jobExecution) {
 // handle potential exceptions
 if (jobExecution.getStatus() == BatchStatus.FAILED) {
 // log the exception and perform any necessary cleanup
 }
 }
 };
 }
}

By following these guidelines and avoiding common mistakes, you can ensure that your Spring Batch multi-step job is properly configured and can handle potential errors. For further reading on Spring Batch and its features, see our article on Introduction to Spring Batch.

Production Tips for Spring Batch Multi-Step Job

When deploying Spring Batch jobs in production, it is essential to consider best practices for monitoring and maintaining these jobs. One crucial aspect is implementing conditional flow to handle different scenarios that may arise during job execution.

Production tip: Use JobExecutionListener to monitor job execution and take necessary actions in case of failures or other events.

To achieve this, you can use the JobExecutionListener interface, which provides methods to handle job execution events. For example, you can use the afterJob method to send notifications or update job status.

public class JobExecutionListenerImpl implements JobExecutionListener {
 @Override
 public void beforeJob(JobExecution jobExecution) {
 // Initialize job execution context
 }
 
 @Override
 public void afterJob(JobExecution jobExecution) {
 // Send notification or update job status
 if (jobExecution.getStatus() == BatchStatus.FAILED) {
 // Handle job failure
 }
 }
}

For more information on implementing job listeners, refer to our article on Spring Batch Job Listeners.

Production tip: Use logging to track job execution and identify potential issues.

To demonstrate the usage of JobExecutionListener, consider the following example:

package com.example.springbatch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfig {
 @Autowired
 private JobBuilderFactory jobBuilderFactory;
 
 @Autowired
 private StepBuilderFactory stepBuilderFactory;
 
 @Bean
 public Job job() {
 return jobBuilderFactory.get("job")
 .incrementer(new RunIdIncrementer())
 .listener(new JobExecutionListenerImpl()) // Register job execution listener
 .start(step())
 .end()
 .build();
 }
 
 @Bean
 public Step step() {
 return stepBuilderFactory.get("step")
 .tasklet((contribution, chunkContext) -> {
 // Tasklet implementation
 return RepeatStatus.FINISHED;
 })
 .build();
 }
}

The expected output will be:

Job execution started
Job execution completed with status: COMPLETED

By following these best practices and using Spring Batch features such as JobExecutionListener and logging, you can ensure reliable and maintainable batch job execution in production environments. For further reading on Spring Batch and its features, refer to our article on Spring Batch Tutorial.

Testing Spring Batch Multi-Step Job with Conditional Flow

When testing **Spring Batch** jobs, it's essential to cover both unit testing and integration testing strategies. Unit testing focuses on individual components, such as **ItemReader** and **ItemWriter**, while integration testing verifies the entire job flow. For a multi-step job with conditional flow, testing becomes more complex due to the dynamic nature of the job execution.

To unit test a **Job**, you can use a testing framework like **JUnit** and mock the required dependencies. For example, you can test the **JobLauncher** to verify that the job is launched correctly. For more information on setting up a **Spring Batch** project, refer to our article on [Configuring Spring Batch](/configuring-spring-batch).

When writing integration tests for a **Spring Batch** job, you should test the entire job flow, including the conditional flow. You can use the **JobLauncherTestUtils** class to launch the job and verify the expected output.

public class JobIntegrationTest {
 
 @Autowired
 private JobLauncherTestUtils jobLauncherTestUtils;
 
 @Test
 public void testJob() throws Exception {
 // Launch the job
 JobExecution execution = jobLauncherTestUtils.launchJob();
 
 // Verify the job execution status
 assertEquals(BatchStatus.COMPLETED, execution.getStatus());
 }
}

The expected output of the above test would be:

Job execution status: COMPLETED

To test the conditional flow, you can use a **JobExecutionListener** to verify that the expected steps are executed. For further reading on **Spring Batch** listeners, see our article on [Using Spring Batch Listeners](/using-spring-batch-listeners).

In addition to testing the job flow, you should also test the **ItemProcessor** and **ItemWriter** components to ensure they are working correctly. You can use a testing framework like **Mockito** to mock the required dependencies and verify the expected behavior. By covering both unit testing and integration testing, you can ensure that your **Spring Batch** job is working correctly and reliably.

Key Takeaways for Implementing Spring Batch Multi-Step Job

When implementing a **Spring Batch** multi-step job, it is essential to understand the concept of **job execution** and **step execution**. A job consists of one or more steps, and each step represents a single unit of work. The **JobBuilderFactory** and **StepBuilderFactory** are used to create jobs and steps, respectively.

To implement a multi-step job with conditional flow, you need to use the **JobExecutionDecider** interface, which allows you to decide the next step based on the previous step's execution result. For more information on **job configuration**, refer to our article on [Configuring a Spring Batch Job](/configuring-spring-batch-job).

The following example demonstrates a simple multi-step job with conditional flow:

package com.example.springbatch;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class MultiStepJobConfig {
 @Autowired
 private JobBuilderFactory jobBuilderFactory;

 @Autowired
 private StepBuilderFactory stepBuilderFactory;

 @Bean
 public Job multiStepJob() {
 // Create a job with two steps
 return jobBuilderFactory.get("multiStepJob")
 .start(step1())
 .next(decider())
 .from(decider())
 .on("FAILED")
 .to(step2())
 .from(decider())
 .on("COMPLETED")
 .to(step3())
 .end()
 .build();
 }

 @Bean
 public Step step1() {
 // Step 1: Read data from a file
 return stepBuilderFactory.get("step1")
 .tasklet((contribution, chunkContext) -> {
 // Read data from a file
 System.out.println("Reading data from a file");
 return RepeatStatus.FINISHED;
 })
 .build();
 }

 @Bean
 public JobExecutionDecider decider() {
 // Decider to decide the next step based on the previous step's execution result
 return new MyDecider();
 }

 @Bean
 public Step step2() {
 // Step 2: Process data
 return stepBuilderFactory.get("step2")
 .tasklet((contribution, chunkContext) -> {
 // Process data
 System.out.println("Processing data");
 return RepeatStatus.FINISHED;
 })
 .build();
 }

 @Bean
 public Step step3() {
 // Step 3: Write data to a file
 return stepBuilderFactory.get("step3")
 .tasklet((contribution, chunkContext) -> {
 // Write data to a file
 System.out.println("Writing data to a file");
 return RepeatStatus.FINISHED;
 })
 .build();
 }
}

class MyDecider implements JobExecutionDecider {
 @Override
 public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
 // Decide the next step based on the previous step's execution result
 if (stepExecution.getStatus() == BatchStatus.FAILED) {
 return new FlowExecutionStatus("FAILED");
 } else {
 return new FlowExecutionStatus("COMPLETED");
 }
 }
}

The expected output of this job will be:

Reading data from a file
Processing data
Writing data to a file

For further reading on **step execution listeners**, refer to our article on [Using Step Execution Listeners in Spring Batch](/using-step-execution-listeners-spring-batch).

Configuring Conditional Flow in Spring Batch Multi-Step Job

To configure conditional flow in a Spring Batch multi-step job, you need to use the JobExecutionDecider interface. This interface allows you to decide the next step in the job flow based on certain conditions. The decide method of this interface returns a FlowExecutionStatus object that determines the next step.

The conditional flow is typically used to handle different scenarios based on the outcome of a previous step. For example, if a step fails, you may want to skip to a recovery step. To achieve this, you can use the JobExecutionDecider to evaluate the exit status of the previous step and return a FlowExecutionStatus that corresponds to the next step.

Here is an example of how to use the JobExecutionDecider to configure conditional flow:

public class MyJobExecutionDecider implements JobExecutionDecider {
 @Override
 public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
 // Check the exit status of the previous step
 String exitStatus = stepExecution.getExitStatus().getExitCode();
 // If the previous step failed, return a FlowExecutionStatus that corresponds to the recovery step
 if (exitStatus.equals("FAILED")) {
 return new FlowExecutionStatus("RECOVERY");
 } else {
 // If the previous step was successful, return a FlowExecutionStatus that corresponds to the next step
 return new FlowExecutionStatus("SUCCESS");
 }
 }
}

For more information on job execution and how to handle different scenarios, you can refer to our previous article on job execution in Spring Batch. The expected output of the above code will be:

RECOVERY

if the previous step failed, or:

SUCCESS

if the previous step was successful. You can use this JobExecutionDecider in your job configuration to configure the conditional flow.

Read Next

Pillar Guide: Spring Batch Complete Guide — explore the full learning path.

Source Code on GitHub
spring-batch-examples — Clone, Star & Contribute

You Might Also Like

Building AI Chatbot with Spring Boot and ChatGPT
Mastering Spring Security Password Encoding with BCrypt Tutorial
Building Scalable Systems with Event Driven Architecture using Spring Boot and Kafka


Leave a Reply

Your email address will not be published. Required fields are marked *