Spring Boot Cheatsheet

Spring Data JPA

Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Entity Basics

@Entity
@Table(name = "products")
@Getter @Setter   // Lombok
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment (DB-level)
    private Long id;

    @Column(name = "product_name", nullable = false, length = 255)
    private String name;

    @Column(unique = true)
    private String sku;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Enumerated(EnumType.STRING)   // store enum name, not ordinal
    private Status status;

    @Lob
    private String description;    // large text

    @CreationTimestamp
    private Instant createdAt;

    @UpdateTimestamp
    private Instant updatedAt;

    @Version
    private Long version;          // optimistic locking
}

@GeneratedValue Strategies

StrategyWhen to Use
IDENTITYMySQL, PostgreSQL auto-increment
SEQUENCEPostgreSQL sequences (better batching)
UUIDGlobally unique IDs
AUTOJPA picks (avoid — behavior varies)
// UUID primary key
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Relationships

@ManyToOne / @OneToMany

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
}

Always use FetchType.LAZY for @ManyToOne — EAGER is the JPA default and causes N+1 problems.

@ManyToMany

@Entity
public class Post {
    @ManyToMany
    @JoinTable(
        name = "post_tags",
        joinColumns = @JoinColumn(name = "post_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private Set<Tag> tags = new HashSet<>();
}

@OneToOne

@Entity
public class UserProfile {
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

Cascade Types

TypeEffect
PERSISTSave child with parent
MERGEMerge child with parent
REMOVEDelete child with parent
REFRESHRefresh child with parent
ALLAll of the above
DETACHDetach child with parent

Repository Interfaces

// Basic CRUD
public interface ProductRepository extends JpaRepository<Product, Long> {}

// With findBy / deleteBy / countBy / existsBy
public interface ProductRepository extends JpaRepository<Product, Long> {

    List<Product>         findByStatus(Status status);
    Optional<Product>     findBySku(String sku);
    boolean               existsBySku(String sku);
    long                  countByStatus(Status status);
    void                  deleteByStatus(Status status);

    // Derived query keywords: And, Or, Between, LessThan, GreaterThan,
    // Like, Containing, StartingWith, EndingWith, In, NotIn,
    // IsNull, IsNotNull, OrderBy, Asc, Desc
    List<Product> findByPriceBetweenAndStatusOrderByPriceAsc(
                      BigDecimal min, BigDecimal max, Status status);

    // Limit results
    List<Product>  findTop5ByStatusOrderByCreatedAtDesc(Status status);
    Optional<Product> findFirstBySkuStartingWith(String prefix);

    // Pagination
    Page<Product>  findByStatus(Status status, Pageable pageable);
    Slice<Product> findByCategory(String category, Pageable pageable); // no total count

    // Projections (interface)
    List<ProductSummary> findByStatus(Status status, Class<ProductSummary> type);
}

JPQL and Native Queries

// JPQL (entity-based, portable)
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice AND p.status = :status")
List<Product> findCheapByStatus(@Param("maxPrice") BigDecimal maxPrice,
                                @Param("status") Status status);

// Native SQL
@Query(value = "SELECT * FROM products WHERE LOWER(name) LIKE LOWER(:pattern)",
       nativeQuery = true)
List<Product> searchByNameNative(@Param("pattern") String pattern);

// Modifying query
@Modifying
@Transactional
@Query("UPDATE Product p SET p.status = :status WHERE p.id IN :ids")
int bulkUpdateStatus(@Param("status") Status status, @Param("ids") List<Long> ids);

// Dynamic count query for pagination
@Query(value = "SELECT p FROM Product p WHERE p.category = :cat",
       countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :cat")
Page<Product> findByCategory(@Param("cat") String cat, Pageable pageable);

Specifications (Dynamic Queries)

public interface ProductRepository
        extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {}
public class ProductSpecs {
    public static Specification<Product> hasStatus(Status status) {
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }

    public static Specification<Product> nameLike(String pattern) {
        return (root, query, cb) -> cb.like(cb.lower(root.get("name")),
                                            "%" + pattern.toLowerCase() + "%");
    }
}

// Usage
List<Product> results = repository.findAll(
    Specification.where(ProductSpecs.hasStatus(Status.ACTIVE))
                 .and(ProductSpecs.nameLike("spring")));

Projections

// Interface projection — Spring generates proxy at runtime
public interface ProductSummary {
    Long getId();
    String getName();
    BigDecimal getPrice();
}

List<ProductSummary> summaries = repository.findByStatus(Status.ACTIVE, ProductSummary.class);

// DTO projection — constructor expression
@Query("SELECT new com.example.dto.ProductDto(p.id, p.name, p.price) FROM Product p")
List<ProductDto> findAllDtos();

// Record DTO (Spring Data 3+)
public record ProductDto(Long id, String name, BigDecimal price) {}

@Query("SELECT new com.example.dto.ProductDto(p.id, p.name, p.price) FROM Product p")
List<ProductDto> findAllDtos();

Transactions

// Service layer — place @Transactional here, never on repository
@Service
@Transactional(readOnly = true)   // default for reads (performance hint)
public class ProductService {

    public ProductDto findById(Long id) { ... }   // uses readOnly tx

    @Transactional    // overrides class-level for writes
    public ProductDto create(CreateProductRequest req) { ... }

    @Transactional(
        propagation = Propagation.REQUIRES_NEW,   // new tx regardless
        isolation   = Isolation.READ_COMMITTED,
        timeout     = 5,                          // seconds
        rollbackFor = {IOException.class}
    )
    public void importBatch(List<CreateProductRequest> items) { ... }
}

Propagation Types

PropagationBehavior
REQUIREDJoin existing or create new (default)
REQUIRES_NEWAlways create a new, suspend current
SUPPORTSJoin if exists, non-transactional otherwise
NOT_SUPPORTEDSuspend current, run non-transactional
MANDATORYMust have existing tx or throws
NEVERMust NOT have tx or throws
NESTEDSavepoint inside existing tx

Auditing

// Enable in @Configuration
@EnableJpaAuditing

// Entity mixin or base class
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
    @CreatedDate
    private Instant createdAt;

    @LastModifiedDate
    private Instant updatedAt;

    @CreatedBy
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;
}

// Provide the current user
@Bean
public AuditorAware<String> auditorProvider() {
    return () -> Optional.ofNullable(SecurityContextHolder.getContext())
        .map(ctx -> ctx.getAuthentication())
        .filter(a -> a != null && a.isAuthenticated())
        .map(Authentication::getName);
}

application.properties JPA Settings

# DDL (create, create-drop, validate, update, none)
spring.jpa.hibernate.ddl-auto=validate          # use Flyway/Liquibase in prod

# Show SQL
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Dialect (usually auto-detected)
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

# Batch inserts
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

# Open Session in View (disable for REST APIs)
spring.jpa.open-in-view=false

Common Pitfalls

  • N+1 queries — use @EntityGraph, JOIN FETCH in JPQL, or @BatchSize to avoid per-entity lazy loads.
  • LazyInitializationException — accessing a lazy relation outside a transaction; fix by fetching eagerly in the query or expanding the transaction boundary.
  • detached entity passed to persist — passing an entity with a set ID to save() when the entity is not managed; either merge it or don't set the ID.
  • open-in-view=true (default) — keeps the Hibernate session open for the entire HTTP request (through the view rendering); disable it in REST APIs.
  • @Transactional on the same class — self-invocation bypasses the proxy; calling a @Transactional method from within the same bean won't start a transaction. Inject the bean into itself or use AopContext.currentProxy().
  • @GeneratedValue(AUTO) with sequences — Hibernate may allocate IDs in large batches (e.g., 50) causing apparent gaps; use SEQUENCE with a named sequence to control allocation size.