java 46 lines · 8 steps

Rebuilding a search index at Spring startup

An ApplicationRunner streams every active product and writes it into a search view in fixed-size batches when the app boots.

Explained by highlit
1@Component
2@Order(20)
3public class ProductSearchIndexRunner implements ApplicationRunner {
4 
5 private static final Logger log = LoggerFactory.getLogger(ProductSearchIndexRunner.class);
6 
7 private final ProductRepository products;
8 private final ProductSearchView searchView;
9 
10 public ProductSearchIndexRunner(ProductRepository products, ProductSearchView searchView) {
11 this.products = products;
12 this.searchView = searchView;
13 }
14 
15 @Override
16 public void run(ApplicationArguments args) {
17 if (args.containsOption("skip-index")) {
18 log.info("Skipping product search index rebuild (--skip-index present)");
19 return;
20 }
21 
22 StopWatch watch = new StopWatch();
23 watch.start();
24 searchView.clear();
25 
26 int indexed = 0;
27 try (Stream<Product> stream = products.streamAllActive()) {
28 List<ProductDocument> batch = new ArrayList<>(500);
29 for (Product product : (Iterable<Product>) stream::iterator) {
30 batch.add(ProductDocument.from(product));
31 if (batch.size() == 500) {
32 searchView.saveAll(batch);
33 indexed += batch.size();
34 batch.clear();
35 }
36 }
37 if (!batch.isEmpty()) {
38 searchView.saveAll(batch);
39 indexed += batch.size();
40 }
41 }
42 
43 watch.stop();
44 log.info("Rebuilt product search index: {} documents in {} ms", indexed, watch.getTotalTimeMillis());
45 }
46}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1ApplicationRunner lets you run one-off work after the context is ready, ordered against other runners.
  2. 2Streaming rows and flushing in fixed-size batches keeps memory flat over large datasets.
  3. 3A command-line flag gives operators an escape hatch to skip expensive startup work.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
java
@Component
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
 

Transparent column encryption in Spring & JPA

encryption aes-gcm jpa-converter
Advanced 10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
java
package com.acme.billing.config;
 
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;

Feature-flagged beans with Spring @ConditionalOnProperty

feature-flags conditional-beans strategy-pattern
Intermediate 5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Rebuilding a search index at Spring startup — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code