Inserting 50k rows with Spring Data and JPA
To state the problem:
You have a large sum of operations that are repetitive and mostly similar(?) that are long running, and you want to reduce that run time.

If I have that right:

If order is not a concern You could consider inserting in parallel. If you can represent the 50,000 updates as a List, say, List&lt;InsertCriteriaPojo&gt;, and can represent the insert as a Consumer lambda, like so: ((InsertCriteriaPojo item) -&gt; myDao.insert(item)), then this could be a one line call:

List&lt;InsertCriteriaPojo&gt; items = ...;

items.stream().parallel().map((InsertCriteriaPojo item) -&gt; myDao.insert(item));


This creates a Stream of InsertCriteriaPojos, converts that to a ParallelStream, and them maps each item in the List to a Consumer and runs the Consumer/item tasks in a set of threads belonging to the common ForkJoinPool



While short, there are problems with this

The main problem as explained here is that you can't specify an Executor to ParallelStreams (as of Java 1.8*).

UPDATE: The above is inaccurate. This can be done like so:

List&lt;InsertCriteriaPojo&gt; items = ...;
ForkJoinPool customThreadPool = new ForkJoinPool(threadCount);
customThreadPool.submit(
    () -&gt; items.stream().parallel().map(
        (InsertCriteriaPojo item) -&gt; myDao.insert(item)
    )
).get;




Another, more complex way to run these tasks in parallel is to use an Executor you configure:

@Bean(name = "myExecutor")
public Executor executor(){
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(yourProperties.getCorePoolCount());
    executor.setMaxPoolSize(yourProperties.getMaxPoolCount());
    executor.setQueueCapacity(yourProperties.getMaxQueueSize());
    executor.setThreadNamePrefix(yourProperties.getThreadNamePrefix());
    executor.initialize();
    return threadPoolTaskExecutor;
}


To run your consumers asynchronously:

...
@Autowire
@Qualifier("myExecutor")
Executor executor;
...
List&lt;InsertCriteriaPojo&gt; items = ...
//Make a Future for each task
List&lt;CompletableFuture&lt;Void&gt;&gt; asyncTaskFutures = list.stream().map(
    (InsertCriteriaPojo item) -&gt;{
        CompletableFuture.runAsync(
            () -&gt; myDao.insert(item), executor
        ).handle(
            (asyncVoidFuture, possibleError)-&gt;{
                if(possibleError != null){ //Handle your errors here }
                else{return asyncVoidFuture;}
        })
    }
).collect(Collectors.toList());

//Create a Future that is complete when allOf your task futures are complete
CompletableFuture&lt;Void&gt; process = CompletableFuture.allOf(
    asyncTaskList.toArray(new CompletableFuture[asyncTaskList.size()])
).thenRun(() -&gt; asyncTaskList.stream().map(future -&gt; future.join()));

//Complete the process-level future, blocks until complete
process.get();


While significantly more complex, the second is a more configurable and reusable way to solve your problem, but depending on your case either approach could serve your goal.