---
/plugin marketplace add a-pavithraa/springboot-skills-marketplace/plugin install springboot-architecture@springboot-skills-marketplaceNEVER create repositories for every entity. ALWAYS create repositories only for aggregate roots.
NEVER use complex query method names. ALWAYS use @Query for non-trivial queries.
NEVER use save() blindly. ALWAYS understand persist vs merge semantics (see Vlad Mihalcea's guidance).
Ask:
| Pattern | When | Read |
|---|---|---|
| Simple Repository | Basic CRUD, 1-2 custom queries | - |
| @Query Repository | Multiple filters, joins, sorting | references/query-patterns.md |
| DTO Projection | Read-only, performance-critical | references/dto-projections.md |
| Custom Repository | Complex logic, bulk ops, Criteria API | references/custom-repositories.md |
| CQRS Query Service | Separate read/write, multiple projections | references/cqrs-query-service.md |
Decision criteria:
| Need | Simple | @Query | DTO | Custom | CQRS |
|---|---|---|---|---|---|
| Basic CRUD | ✅ | ✅ | ❌ | ✅ | ✅ |
| Custom Queries | ❌ | ✅ | ✅ | ✅ | ✅ |
| Best Performance | ✅ | ✅ | ✅✅ | ✅✅ | ✅✅ |
| Complex Logic | ❌ | ❌ | ❌ | ✅ | ✅ |
| Read/Write Separation | ❌ | ❌ | ✅ | ✅ | ✅✅ |
For basic lookups (1-2 properties):
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
Optional<ProductEntity> findByCode(String code);
List<ProductEntity> findByStatus(ProductStatus status);
}
Asset: Use existing entity and repository patterns
For 3+ filters, joins, or readability. Read: references/query-patterns.md
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
@Query("""
SELECT DISTINCT o
FROM OrderEntity o
LEFT JOIN FETCH o.items
WHERE o.userId = :userId
ORDER BY o.createdAt DESC
""")
List<OrderEntity> findUserOrders(@Param("userId") Long userId);
}
Asset: assets/query-repository.java - Complete template with examples
For read-only, performance-critical queries. Read: references/dto-projections.md
public record ProductSummary(Long id, String name, BigDecimal price) {}
@Query("""
SELECT new com.example.ProductSummary(p.id, p.name, p.price)
FROM ProductEntity p
WHERE p.status = 'ACTIVE'
""")
List<ProductSummary> findActiveSummaries();
Asset: assets/dto-projection.java - Records, interfaces, native queries
For Criteria API, bulk ops. Read: references/custom-repositories.md
// 1. Custom interface
public interface ProductRepositoryCustom {
List<ProductEntity> findByDynamicCriteria(SearchCriteria criteria);
}
// 2. Implementation (must be named <Repository>Impl)
@Repository
class ProductRepositoryImpl implements ProductRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
// Implementation using Criteria API
}
// 3. Main repository extends both
public interface ProductRepository extends JpaRepository<ProductEntity, Long>,
ProductRepositoryCustom {
Optional<ProductEntity> findBySku(String sku);
}
Asset: assets/custom-repository.java - Complete pattern
For Tomato/DDD architectures. Read: references/cqrs-query-service.md
// Repository (package-private) - writes only
interface ProductRepository extends JpaRepository<ProductEntity, ProductId> {
Optional<ProductEntity> findBySku(ProductSKU sku);
}
// QueryService (public) - reads only
@Service
@Transactional(readOnly = true)
public class ProductQueryService {
private final JdbcTemplate jdbcTemplate;
public List<ProductVM> findAllActive() {
return jdbcTemplate.query("""
SELECT id, name, price FROM products
WHERE status = 'ACTIVE'
""",
(rs, rowNum) -> new ProductVM(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price")
)
);
}
}
Asset: assets/query-service.java - Full CQRS pattern with JdbcTemplate
Read: references/relationships.md for detailed guidance
Quick patterns:
// ✅ GOOD: @ManyToOne (most common)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
// ✅ ALTERNATIVE: Just use ID (loose coupling)
@Column(name = "product_id", nullable = false)
private Long productId;
// ❌ AVOID: @OneToMany (query from many side instead)
// Instead: List<OrderItem> items = itemRepository.findByOrderId(orderId);
// ❌ NEVER: @ManyToMany (create join entity instead)
@Entity
public class Enrollment {
@ManyToOne private Student student;
@ManyToOne private Course course;
private LocalDate enrolledAt;
}
Asset: assets/relationship-patterns.java - All relationship types with examples
Read: references/performance-guide.md for complete checklist
Critical optimizations:
Prevent N+1 queries:
// Use JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findWithCustomer();
// Or use DTO projection
@Query("SELECT new OrderSummary(o.id, c.name) FROM Order o JOIN o.customer c")
List<OrderSummary> findSummaries();
Use pagination:
Pageable pageable = PageRequest.of(0, 20);
Page<Product> page = repository.findByCategory("Electronics", pageable);
Mark read services as readOnly:
@Service
@Transactional(readOnly = true)
public class ProductQueryService { }
Configure batch size:
spring.jpa.properties.hibernate.jdbc.batch_size: 25
Best practices:
@Service
@Transactional(readOnly = true) // Class-level for read services
public class ProductService {
public List<ProductVM> findAll() {
// Read operations
}
@Transactional // Override for writes
public void createProduct(CreateProductCmd cmd) {
ProductEntity product = ProductEntity.create(cmd);
repository.save(product);
}
}
Rules:
@Transactional(readOnly = true) at class level for query services@Transactional at service layer, not repository@Transactional for write methods in read services@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ProductRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private ProductRepository repository;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void shouldFindProductByCode() {
ProductEntity product = createTestProduct("P001");
repository.save(product);
Optional<ProductEntity> found = repository.findByCode("P001");
assertThat(found).isPresent();
}
}
| Don't | Do | Why |
|---|---|---|
| Repository for every entity | Only for aggregate roots | Maintains boundaries |
| Use save() blindly | Understand persist/merge | Avoids unnecessary SELECT |
| Long query method names | Use @Query | Readability |
| findAll() without pagination | Use Page<> or Stream | Memory issues |
| Fetch entities for read views | Use DTO projections | Performance |
| FetchType.EAGER | LAZY + JOIN FETCH | Avoids N+1 |
| @ManyToMany | Use join entity | Allows relationship attributes |
| @Transactional in repository | Put in service layer | Proper boundaries |
| Return entities from controllers | Return DTOs/VMs | Prevents lazy issues |
Problem: Accessing lazy associations outside transaction
Solution: Use DTO projection or JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
Problem: Loading associations in loop
Solution: See references/performance-guide.md
Problem: Multiple JOIN FETCH with collections
Solution: Separate queries or DTO projections
references/query-patterns.mdreferences/dto-projections.mdreferences/custom-repositories.mdreferences/cqrs-query-service.mdreferences/relationships.mdreferences/performance-guide.mdAll templates in assets/:
query-repository.java - @Query examples, pagination, bulk opsdto-projection.java - Records, interfaces, native queriescustom-repository.java - Criteria API, EntityManagerquery-service.java - CQRS with JdbcTemplaterelationship-patterns.java - All JPA associationsIncorporates best practices from:
Browse vladmihalcea.com/blog for deep-dive articles.
Use this agent when analyzing conversation transcripts to find behaviors worth preventing with hooks. Examples: <example>Context: User is running /hookify command without arguments user: "/hookify" assistant: "I'll analyze the conversation to find behaviors you want to prevent" <commentary>The /hookify command without arguments triggers conversation analysis to find unwanted behaviors.</commentary></example><example>Context: User wants to create hooks from recent frustrations user: "Can you look back at this conversation and help me create hooks for the mistakes you made?" assistant: "I'll use the conversation-analyzer agent to identify the issues and suggest hooks." <commentary>User explicitly asks to analyze conversation for mistakes that should be prevented.</commentary></example>