Configuration, JPA data access and profiles

Layered properties, environment-specific profiles, and a repository layer with the traps called out.

Configuration and profiles

# src/main/resources/application.properties
spring.application.name=demo
server.port=8080
spring.jpa.open-in-view=false
app.greeting=Hello
# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demo
    username: demo
    password: ${DB_PASSWORD}     # read from the environment, never committed
  jpa:
    hibernate:
      ddl-auto: validate
@Component
@ConfigurationProperties(prefix = "app")
class AppProperties {
    private String greeting = "";
    // getters and setters
}

@Configuration
@Profile("prod")
class ProdCacheConfig {
    // only active when the prod profile is on
}
  • Resolution order, later wins: application.properties, then the profile-specific file, then environment variables, then command-line arguments.
  • Placeholders read other properties or environment variables, which keeps secrets out of the repository. Inject real credentials at deploy time.
  • Profiles select a set of configuration; they do not change code by themselves, so keep the differences declarative.
  • Activate with --spring.profiles.active=prod, the SPRING_PROFILES_ACTIVE environment variable, or spring.profiles.active in the base file.

Data access with Spring Data JPA

@Entity
@Table(name = "users")
class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    // getters and setters omitted
}

interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmailIgnoreCase(String email);

    @Query("select u from User u join fetch u.orders where u.id = :id")
    Optional<User> findWithOrders(long id);
}
ConcernWhat to do
Connection poolHikariCP is the default; size it to the database, not to request volume
Schema changesFlyway or Liquibase in production; never rely on ddl-auto=update
N+1 selectsUse a fetch join or @EntityGraph for collections you actually render
Lazy loadingFetch inside the service, or project to a DTO, rather than touching relations in a template
TransactionsAnnotate the service method, and mark read-only queries as such
PaginationReturn Page<T> from the repository instead of loading every row
⚠️
spring.jpa.hibernate.ddl-auto=update is convenient in development and dangerous in production: it never drops columns, and there is no review or rollback. Use a migration tool and set validate in production.

FAQ

Do I need a database running to start the app?
No. Add H2 with runtime scope and Spring Boot auto-configures an in-memory datasource. The same repository code then works against PostgreSQL once you supply its driver and URL.
What does open-in-view do?
It keeps the Hibernate session open for the whole request so lazy loading works during serialisation. That convenience hides N+1 queries, so most services disable it and fetch explicitly.

REST controllers and dependency injection Project setup and your first controller

Last refreshed 2026-09-18.