package com.example.reference;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "countries")
public class Country {
@Id
@Column(length = 2, nullable = false)
private String code;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String currency;
protected Country() {
}
public Country(String code, String name, String currency) {
this.code = code;
this.name = name;
this.currency = currency;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public String getCurrency() {
return currency;
}
}
package com.example.reference;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CountryRepository extends JpaRepository<Country, String> {
}
package com.example.reference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.InputStream;
import java.util.List;
@Component
@Order(1)
@Profile("!test")
public class ReferenceDataSeeder implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(ReferenceDataSeeder.class);
private final CountryRepository countries;
private final ObjectMapper objectMapper;
public ReferenceDataSeeder(CountryRepository countries, ObjectMapper objectMapper) {
this.countries = countries;
this.objectMapper = objectMapper;
}
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
if (countries.count() > 0) {
log.info("Reference data already present ({} countries), skipping seed", countries.count());
return;
}
List<CountryRecord> records = readCountries();
List<Country> entities = records.stream()
.map(r -> new Country(r.code(), r.name(), r.currency()))
.toList();
countries.saveAll(entities);
log.info("Seeded {} countries from reference/countries.json", entities.size());
}
private List<CountryRecord> readCountries() throws Exception {
ClassPathResource resource = new ClassPathResource("reference/countries.json");
try (InputStream in = resource.getInputStream()) {
return objectMapper.readValue(in, objectMapper.getTypeFactory()
.constructCollectionType(List.class, CountryRecord.class));
}
}
private record CountryRecord(String code, String name, String currency) {
}
}
[
{ "code": "US", "name": "United States", "currency": "USD" },
{ "code": "GB", "name": "United Kingdom", "currency": "GBP" },
{ "code": "DE", "name": "Germany", "currency": "EUR" },
{ "code": "JP", "name": "Japan", "currency": "JPY" },
{ "code": "BR", "name": "Brazil", "currency": "BRL" },
{ "code": "IN", "name": "India", "currency": "INR" }
]
This snippet shows a common bootstrap pattern in Spring Boot: loading immutable reference data (countries, currencies, categories) exactly once when the application starts, so lookups later hit a warm, populated table instead of an empty one. Reference data rarely changes but is required by the rest of the domain, and an ApplicationRunner is the idiomatic hook for running such work after the ApplicationContext is fully initialized but before the app begins serving requests.
In Country, the entity uses the ISO alpha-2 code as its natural @Id rather than a generated surrogate key. Reference data has a stable business identity, so keying on the code makes the seed idempotent by construction: inserting the same code twice is a primary-key collision, and the seeder can decide whether a row already exists cheaply. The name column is marked nullable = false to keep the table self-consistent.
ReferenceDataSeeder is annotated @Component so Spring registers it, and it implements ApplicationRunner, whose run(ApplicationArguments) fires once at startup. The @Order(1) ensures it runs before other runners that might depend on the data. The whole run method is @Transactional, so either the full seed commits or nothing does — partial reference data is worse than none. Crucially the method is guarded by count(): if rows already exist it logs and returns, making restarts and repeated deploys safe (idempotency). The @Profile("!test") annotation keeps this out of the test context, where fixtures are usually loaded differently.
The actual rows come from reference/countries.json on the classpath, read through Jackson's ObjectMapper and Spring's ClassPathResource. Parsing external data instead of hardcoding a giant list keeps the seeder readable and lets non-developers edit the catalog. The parsed records are mapped to entities and persisted with saveAll, batching the inserts.
The trade-off worth noting: an ApplicationRunner seed is simple and version-controlled, but it is not a substitute for a real migration tool like Flyway when the schema or data must evolve with auditable, ordered scripts. This pattern fits small, stable lookup tables owned by the app. The count() guard also means changes to the JSON after the first successful boot are ignored, so it favors immutable catalogs over evolving ones.
Related snips
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
module EmailNormalization
extend ActiveSupport::Concern
included do
attr_accessor :soft_warnings
Soft Validation: Normalize + Validate Email
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
Share this code
Here's the card — post it anywhere.