Stage 4 - CRUD Operations: Create, Read, Update, Delete
CRUD is not just four repository calls; it is the basic data lifecycle wrapped in validation, errors, and transactions. Most business features combine Create, Read, Update, and Delete with validation, authorization, transactions, and domain rules. In Spring Data JPA, repositories provide the low-level CRUD methods, while services decide how those methods are used safely.
Create and Read
Create means inserting a new record. In Java code, you usually create an entity object, fill required fields, validate business rules, and call save. Hibernate generates an insert when the object has no database identity yet.
public User createUser(CreateUserRequest request) {
User user = new User(request.email(), request.name());
return userRepository.save(user);
}
Read means loading data. Reading by id uses findById. Reading a list can use findAll, but production APIs should normally use pagination. Pageable describes page number, page size, and sorting. Page<T> contains the content and metadata such as total elements and total pages.
public Page<Product> listProducts(Pageable pageable) {
return productRepository.findAll(pageable);
}
Update
Update should usually start by loading the existing entity. This matters because the database row may not exist, and because the current state may be needed for validation. After loading the entity inside a transaction, change its fields. Hibernate tracks managed entities and flushes changes at commit.
@Transactional
public User renameUser(Long id, String name) {
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found: " + id));
user.setName(name);
return user;
}
Calling save after changing a managed entity is often not required, but many teams still do it for explicitness. The important point is to avoid updating by creating a new object with only an id and a few fields unless you understand merge behavior. That approach can accidentally overwrite fields with null.
Delete and service layer
Delete removes data. deleteById is concise, but for business operations it is often better to load first, check rules, and then call delete. For example, deleting a product may be forbidden if it has completed orders. Such rules belong in the service layer, not in the controller.
Controllers should translate HTTP input and output. Services should coordinate use cases. Repositories should persist entities. If CRUD code is placed directly in controllers, error handling, validation, and transactions become scattered.
| CRUD scenario | Correct service flow | Common beginner mistake |
|---|---|---|
| Create a user | Validate request, create a new entity, set defaults, call save. | Trusting request fields directly and forgetting required defaults such as createdAt. |
| Read one user | Call findById, convert empty Optional to a domain error or HTTP 404. | Calling .get() and producing a vague runtime exception. |
| Read product list | Accept Pageable, choose sorting, return DTO page. | Calling findAll() and loading the whole table for one screen. |
| Update product price | Load existing entity, check rules, change fields inside a transaction. | Creating a new partial entity and accidentally overwriting fields with null. |
| Delete record | Load entity, verify deletion is allowed, then delete. | Deleting by id without checking ownership, status, or dependent records. |
Error handling
EntityNotFoundException or a custom exception makes missing data explicit. In a REST API, the exception can be mapped to HTTP 404 by a @ControllerAdvice. Custom exceptions are often better when the domain language matters, such as UserNotFoundException or ProductNotFoundException.
Errors should not be swallowed. If an update target does not exist, the API should not silently create another row unless the feature explicitly supports upsert. Clear failures make clients easier to debug and prevent hidden data corruption.
Practice
Implement CRUD for users and products. For users, create endpoints to create, get by id, list with pagination, rename, and delete. For products, include price and availability updates. Keep controllers thin: they call services and return DTOs. Put @Transactional on service methods that modify data. Test missing ids and verify that they produce a predictable error response.
Understanding checklist
- I can explain Create, Read, Update, and Delete in JPA terms.
- I know why update usually begins with
findById. - I understand
PageableandPage. - I know why controllers should not contain CRUD business logic.
- I can design a clear not-found error flow.