Every now and then we need to run some work as soon as the system boots—say, warming a cache by loading hot data from the database into memory.
Different languages and frameworks approach this in different ways.
Java: Spring Boot
You typically implement ApplicationListener<ContextRefreshedEvent> and do the work inside the onApplicationEvent callback.
// https://juejin.cn/post/7459021463329865728
@Component
public class CachePrewarmListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private CacheService cacheService; // Example cache service
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// Cache warm-up logic
System.out.println("Starting cache warm-up...");
List<String> hotData = cacheService.getHotDataFromDatabase(); // Simulate loading hot data
for (String data : hotData) {
cacheService.putIntoCache(data); // Write it to the cache
}
System.out.println("Cache warm-up finished!");
}
}
Node.js: Fastify
Fastify runs it in the app’s ready callback.
app.ready(async (err) => {
if (err) throw err
app.log.info('Starting cache warm-up...')
const hotData = await cacheService.getHotDataFromDatabase()
for (const data of hotData) {
await cacheService.putIntoCache(data)
}
app.log.info('Cache warm-up finished!')
})
Elixir: Phoenix
Phoenix takes a different tack: add a child process under the application’s supervision tree to handle the cache warm-up.
A running Elixir/Phoenix app is a tree of processes (in the BEAM sense, not OS processes). In a typical Phoenix project the root node is the application, and the default children include telemetry/metrics collection, the database integration, routing, node discovery, and so on.
Over time I’ve added custom children for caching (via an open-source library), background jobs (another library), and now a warm-up worker named Dokuya.Translation.DictionaryLoader.
@impl true
def start(_type, _args) do
children = [
DokuyaWeb.Telemetry,
Dokuya.Repo,
{DNSCluster, query: Application.get_env(:dokuya, :dns_cluster_query) || :ignore},
{Oban, Application.fetch_env!(:dokuya, Oban)},
{Phoenix.PubSub, name: Dokuya.PubSub},
DokuyaWeb.Endpoint,
{Cachex, [:translation_dictionary_cache]},
Dokuya.Translation.DictionaryLoader
]
opts = [strategy: :one_for_one, name: Dokuya.Supervisor]
Supervisor.start_link(children, opts)
end
Each child must be a GenServer, so Dokuya.Translation.DictionaryLoader looks like this:
defmodule Dokuya.Translation.DictionaryLoader do
use GenServer
require Logger
alias Dokuya.Translation.Dictionary
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(init_arg) do
Logger.info("Starting cache warm-up")
dictionaries =
Dictionary.load_all()
|> Enum.map(fn dict ->
{dict.text, dict}
end)
Cachex.put_many!(:translation_dictionary_cache, dictionaries)
Logger.info("Loaded dictionary data into memory")
{:ok, init_arg}
end
end
The logic is straightforward boilerplate—the warm-up lives entirely in the init callback.