Common Patterns¶
Best practices and common patterns for Zygarde applications.
Entity Patterns¶
Using Entity Base Classes¶
Choose the appropriate base class for your ID strategy:
// Auto-increment Long ID (recommended for most cases)
@Entity
class Book(var title: String) : AutoLongIdEntity()
// Auto-increment Int ID
@Entity
class Category(var name: String) : AutoIntIdEntity()
// UUID ID
@Entity
class SecureEntity(var data: String) : AutoIdEntity<UUID>() {
@Id @GeneratedValue
override var id: UUID? = null
}
Auditing with Envers¶
Track entity changes automatically:
@Entity
@Audited
class AuditedBook(
var title: String,
var author: String
) : AuditAutoLongIdEntity()
// Inherited fields: createdAt, createdBy, lastModifiedAt, lastModifiedBy
Service Layer Patterns¶
Service with Aggregated Dao¶
Use the aggregated Dao for clean service code:
@Service
class BookService(private val dao: Dao) {
@Transactional(readOnly = true)
fun getBookWithAuthor(id: Long): BookDto {
val book = dao.book.findById(id).orElseThrow()
val author = book.author
return book.toBookDto(author.toAuthorDto())
}
}
Transaction Management¶
Use appropriate transaction settings:
@Service
class BookService(private val dao: Dao) {
// Read-only for queries
@Transactional(readOnly = true)
fun searchBooks(criteria: SearchCriteria): List<BookDto> {
return dao.book.search {
/* search criteria */
}.map { it.toBookDto() }
}
// Read-write for modifications
@Transactional
fun createBook(request: CreateBookReq): BookDto {
val book = Book().applyFrom(request)
return dao.book.save(book).toBookDto()
}
}
DAO Patterns¶
Search with Pagination¶
Combine search DSL with pagination:
fun searchBooks(
title: String?,
pageable: Pageable
): Page<BookDto> {
val spec = EnhancedSearchImpl<Book>().apply {
title?.let { title() containsIgnoreCase it }
}.toSpecification()
return dao.book.findAll(spec, pageable)
.map { it.toBookDto() }
}
Reusable Specifications¶
Create reusable search specifications:
object BookSpecs {
fun published() = EnhancedSearchImpl<Book>().apply {
status() eq Status.PUBLISHED
}.toSpecification()
fun byAuthor(authorId: Long) = EnhancedSearchImpl<Book>().apply {
author().id() eq authorId
}.toSpecification()
fun recentBooks(days: Int) = EnhancedSearchImpl<Book>().apply {
createdAt() gte LocalDateTime.now().minusDays(days.toLong())
}.toSpecification()
}
// Combine specifications
val books = dao.book.findAll(
BookSpecs.published()
.and(BookSpecs.recentBooks(30))
)
DTO Patterns¶
Separation of Concerns¶
Use different DTOs for different operations:
// Read DTO - all fields
data class BookDto(
val id: Long,
val title: String,
val description: String?,
val price: Double,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime
)
// Create DTO - user-provided fields only
data class CreateBookReq(
val title: String,
val description: String?,
val price: Double
)
// Update DTO - updatable fields only
data class UpdateBookReq(
val title: String?,
val description: String?,
val price: Double?
)
// Summary DTO - minimal fields for lists
data class BookSummaryDto(
val id: Long,
val title: String,
val price: Double
)
Hierarchical DTOs¶
Use inheritance for shared fields:
// Base DTO
abstract class BaseEntityDto(
open val id: Long,
open val createdAt: LocalDateTime,
open val updatedAt: LocalDateTime
)
// Specific DTOs
data class BookDto(
override val id: Long,
val title: String,
val price: Double,
override val createdAt: LocalDateTime,
override val updatedAt: LocalDateTime
) : BaseEntityDto(id, createdAt, updatedAt)
Controller Patterns¶
Standard CRUD Controller¶
Implement consistent CRUD operations:
@RestController
@RequestMapping("/api/books")
class BookController(private val bookService: BookService) {
@GetMapping
fun list(@PageableDefault(size = 20) pageable: Pageable) =
bookService.getAllBooks(pageable)
@GetMapping("/{id}")
fun get(@PathVariable id: Long) =
bookService.getBookById(id)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun create(@Valid @RequestBody request: CreateBookReq) =
bookService.createBook(request)
@PutMapping("/{id}")
fun update(@PathVariable id: Long, @Valid @RequestBody request: UpdateBookReq) =
bookService.updateBook(id, request)
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun delete(@PathVariable id: Long) =
bookService.deleteBook(id)
}
Search Endpoint¶
Provide flexible search capabilities:
@GetMapping("/search")
fun search(
@RequestParam(required = false) title: String?,
@RequestParam(required = false) authorId: Long?,
@RequestParam(required = false) minPrice: Double?,
@RequestParam(required = false) maxPrice: Double?,
@PageableDefault(size = 20, sort = ["title"]) pageable: Pageable
): PageDto<BookDto> {
return bookService.searchBooks(
title = title,
authorId = authorId,
minPrice = minPrice,
maxPrice = maxPrice,
pageable = pageable
)
}
Exception Handling Patterns¶
Domain Exceptions¶
Create domain-specific exceptions:
sealed class BookException(message: String, code: String) :
BusinessException(message, code)
class BookNotFoundException(id: Long) :
BookException("Book not found: $id", "BOOK_NOT_FOUND")
class InvalidISBNException(isbn: String) :
BookException("Invalid ISBN: $isbn", "INVALID_ISBN")
class BookAlreadyExistsException(isbn: String) :
BookException("Book with ISBN $isbn already exists", "BOOK_EXISTS")
Exception Mapping¶
Map exceptions globally:
@RestControllerAdvice
class GlobalExceptionHandler : ApiExceptionHandler() {
override fun getExceptionMappers() = listOf(
NoSuchElementExceptionMapper(),
IllegalArgumentExceptionMapper(),
ConstraintViolationExceptionMapper()
)
}
class NoSuchElementExceptionMapper :
ExceptionToBusinessExceptionMapper<NoSuchElementException> {
override val exceptionClass = NoSuchElementException::class.java
override fun map(exception: NoSuchElementException) =
BusinessException(
message = exception.message ?: "Resource not found",
code = "NOT_FOUND"
)
}
Validation Patterns¶
Entity-Level Validation¶
@Entity
class Book(
@Column(nullable = false)
@field:NotBlank
@field:Size(min = 1, max = 200)
var title: String,
@Column(nullable = false)
@field:DecimalMin("0.01")
@field:DecimalMax("9999.99")
var price: Double
) : AutoLongIdEntity()
DTO-Level Validation¶
data class CreateBookReq(
@field:NotBlank(message = "Title is required")
@field:Size(min = 1, max = 200, message = "Title must be 1-200 characters")
val title: String,
@field:NotNull(message = "Price is required")
@field:DecimalMin(value = "0.01", message = "Price must be positive")
@field:DecimalMax(value = "9999.99", message = "Price must be less than 10000")
val price: Double,
@field:ISBN
val isbn: String?
)
Testing Patterns¶
Service Tests¶
@SpringBootTest
class BookServiceTest {
@Autowired
lateinit var bookService: BookService
@Autowired
lateinit var dao: Dao
@Test
@Transactional
fun `should create and retrieve book`() {
// given
val request = CreateBookReq(
title = "Test Book",
price = 29.99
)
// when
val created = bookService.createBook(request)
val retrieved = bookService.getBookById(created.id!!)
// then
retrieved.title shouldBe "Test Book"
retrieved.price shouldBe 29.99
}
@AfterEach
fun cleanup() {
dao.book.deleteAll()
}
}
Controller Tests¶
@WebMvcTest(BookController::class)
class BookControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@MockkBean
lateinit var bookService: BookService
@Test
fun `should create book`() {
// given
val request = CreateBookReq(title = "New Book", price = 39.99)
val expected = BookDto(id = 1L, title = "New Book", price = 39.99)
every { bookService.createBook(request) } returns expected
// when & then
mockMvc.post("/api/books") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}.andExpect {
status { isOk() }
jsonPath("$.id") { value(1) }
jsonPath("$.title") { value("New Book") }
}
}
}
Configuration Patterns¶
Application Profiles¶
# application.yml (common settings)
spring:
jpa:
show-sql: false
properties:
hibernate:
format_sql: true
---
# application-dev.yml (development)
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
---
# application-prod.yml (production)
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL}
jpa:
show-sql: false
hibernate:
ddl-auto: validate
Performance Patterns¶
Lazy Loading with Fetch Joins¶
Avoid N+1 queries:
@Query("SELECT b FROM Book b JOIN FETCH b.author WHERE b.id IN :ids")
fun findByIdWithAuthor(ids: List<Long>): List<Book>
Batch Operations¶
Process large datasets efficiently:
@Transactional
fun importBooks(books: List<Book>) {
books.chunked(100).forEach { batch ->
dao.book.saveAll(batch)
dao.book.flush()
}
}
Projection Queries¶
Fetch only needed fields:
interface BookSummary {
val id: Long
val title: String
}
@Query("SELECT b.id as id, b.title as title FROM Book b WHERE b.status = :status")
fun findSummariesByStatus(status: Status): List<BookSummary>
See Also¶
- JPA Extensions → - JPA features
- Search DSL → - Query patterns
- Web & REST → - API patterns
- Tutorials → - Complete examples