π Minimal Java/Spring Core Flow (with Examples)¶
β REST basics¶
@RestController
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping
public String sayHello() {
return "Hello, World!";
}
}
β DTOs (use instead of Entities for I/O)¶
// Input DTO with validation
public record UserDto(
@NotBlank String name,
@Email String email
) {}
// In controller
@PostMapping("/users")
public UserDto createUser(@Valid @RequestBody UserDto dto) {
return dto; // later convert to Entity, save, return response DTO
}
π Note: Use DTOs for input validation and API responses. Donβt expose Entities directly β keep Entities for persistence logic only.
π Why DTOs instead of Entities for input/return?
- Entities = your database model (tables, persistence rules).
- DTOs (Data Transfer Objects) = what you expose to the outside world (API requests & responses).
Why not use entities directly?¶
- Security β prevents exposing internal DB fields (e.g. passwords, IDs).
- Flexibility β API contract can evolve without breaking your DB schema.
- Validation β DTOs are designed for request/response shape, so they fit
@Validchecks. - Separation of concerns β Entities = persistence, DTOs = communication. Keeps your code cleaner.
Typical pattern¶
- Incoming request: DTO with
@Valid(only the fields you expect from client). - Service layer: Map DTO β Entity, persist.
- Outgoing response: DTO again (so you donβt return raw Entity).
β Service + Dependency Injection¶
@Service
public class UserService {
public String welcomeUser(String name) {
return "Welcome, " + name;
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@GetMapping("/{name}")
public String welcome(@PathVariable String name) {
return userService.welcomeUser(name);
}
}
β Entity + Repository¶
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
private String email;
}
public interface UserRepository extends JpaRepository<User, Long> {}
β
application.yml¶
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
β JUnit Test¶
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService service;
@Test
void testWelcomeUser() {
assertEquals("Welcome, Alice", service.welcomeUser("Alice"));
}
}
β Running¶
π Muscle-memory loop:
- Controller β DTO (
@Valid) - Service β business logic
- Entity + Repo β persistence
- Return response DTO
- Config in
application.yml - One test + run
π¦ Product Inventory β Example (Java + Spring Boot)¶
hereβs a broader, realistic example that shows:
- an Entity with more fields,
- DTOs for input/output with validation,
- a Service layer with business rules,
- a Repository and Controller, plus a tiny mapper.
Domain: simple Product inventory.
1) Entity (persistence model)¶
// src/main/java/com/example/product/Product.java
package com.example.product;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "products", indexes = {
@Index(name = "idx_products_sku_unique", columnList = "sku", unique = true)
})
public class Product {
public enum Status { DRAFT, ACTIVE, DISCONTINUED }
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 64, unique = true)
private String sku; // business key, unique
@Column(nullable = false, length = 160)
private String name;
@Column(length = 2000)
private String description;
@Column(nullable = false)
private Long priceCents; // store money as integer
@Column(nullable = false, length = 3)
private String currency; // "EUR", "USD"
@Column(nullable = false)
private Integer stock; // >= 0
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private Status status = Status.DRAFT;
@Column(nullable = false, updatable = false)
private Instant createdAt;
@Column(nullable = false)
private Instant updatedAt;
@PrePersist
void prePersist() {
var now = Instant.now();
createdAt = now;
updatedAt = now;
}
@PreUpdate
void preUpdate() {
updatedAt = Instant.now();
}
// getters/setters omitted for brevity
// ...
}
Note: we keep entity constraints mostly DB-related (nullability, lengths, uniqueness). Validation annotations for requests live on DTOs.
2) DTOs (request/response) + validation¶
// src/main/java/com/example/product/dto/ProductCreateDto.java
package com.example.product.dto;
import jakarta.validation.constraints.*;
public record ProductCreateDto(
@NotBlank @Size(max = 64)
String sku,
@NotBlank @Size(max = 160)
String name,
@Size(max = 2000)
String description,
@NotNull @Positive
Long priceCents,
@NotBlank @Pattern(regexp = "^[A-Z]{3}$", message = "Use ISO currency like EUR/USD")
String currency,
@NotNull @PositiveOrZero
Integer stock
) {}
// src/main/java/com/example/product/dto/ProductUpdateDto.java
package com.example.product.dto;
import jakarta.validation.constraints.*;
import com.example.product.Product.Status;
public record ProductUpdateDto(
@Size(max = 160)
String name,
@Size(max = 2000)
String description,
@Positive
Long priceCents,
@Pattern(regexp = "^[A-Z]{3}$")
String currency,
@PositiveOrZero
Integer stock,
Status status
) {}
// src/main/java/com/example/product/dto/ProductResponseDto.java
package com.example.product.dto;
import java.time.Instant;
import com.example.product.Product.Status;
public record ProductResponseDto(
Long id,
String sku,
String name,
String description,
Long priceCents,
String currency,
Integer stock,
Status status,
Instant createdAt,
Instant updatedAt
) {}
3) Mapper (Entity β DTO)¶
// src/main/java/com/example/product/ProductMapper.java
package com.example.product;
import com.example.product.dto.*;
public class ProductMapper {
public static Product toEntity(ProductCreateDto dto) {
var p = new Product();
p.setSku(dto.sku().trim());
p.setName(dto.name().trim());
p.setDescription(dto.description());
p.setPriceCents(dto.priceCents());
p.setCurrency(dto.currency());
p.setStock(dto.stock());
p.setStatus(Product.Status.DRAFT);
return p;
}
public static void applyUpdate(Product p, ProductUpdateDto dto) {
if (dto.name() != null) p.setName(dto.name().trim());
if (dto.description() != null) p.setDescription(dto.description());
if (dto.priceCents() != null) p.setPriceCents(dto.priceCents());
if (dto.currency() != null) p.setCurrency(dto.currency());
if (dto.stock() != null) p.setStock(dto.stock());
if (dto.status() != null) p.setStatus(dto.status());
}
public static ProductResponseDto toResponse(Product p) {
return new ProductResponseDto(
p.getId(), p.getSku(), p.getName(), p.getDescription(),
p.getPriceCents(), p.getCurrency(), p.getStock(),
p.getStatus(), p.getCreatedAt(), p.getUpdatedAt()
);
}
}
4) Repository¶
// src/main/java/com/example/product/ProductRepository.java
package com.example.product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findBySku(String sku);
boolean existsBySku(String sku);
}
5) Service (business rules)¶
// src/main/java/com/example/product/ProductService.java
package com.example.product;
import com.example.product.dto.*;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository repo;
@Transactional
public ProductResponseDto create(ProductCreateDto dto) {
// business rule: SKU must be unique
if (repo.existsBySku(dto.sku())) {
throw new IllegalArgumentException("SKU already exists: " + dto.sku());
}
// business rule: enforce EUR only (example)
if (!"EUR".equals(dto.currency())) {
throw new IllegalArgumentException("Only EUR supported for now.");
}
var entity = ProductMapper.toEntity(dto);
// business rule: newly created ACTIVE products must have stock > 0
// (we start as DRAFT; activate via update when ready)
var saved = repo.save(entity);
return ProductMapper.toResponse(saved);
}
@Transactional
public ProductResponseDto update(Long id, ProductUpdateDto dto) {
var product = repo.findById(id).orElseThrow(() -> new IllegalArgumentException("Product not found"));
// business rule: stock cannot be negative
if (dto.stock() != null && dto.stock() < 0) {
throw new IllegalArgumentException("Stock cannot be negative");
}
// business rule: if switching to ACTIVE, ensure stock > 0 and has price
if (dto.status() == Product.Status.ACTIVE) {
var newStock = dto.stock() != null ? dto.stock() : product.getStock();
var newPrice = dto.priceCents() != null ? dto.priceCents() : product.getPriceCents();
if (newStock == null || newStock <= 0) throw new IllegalArgumentException("ACTIVE requires stock > 0");
if (newPrice == null || newPrice <= 0) throw new IllegalArgumentException("ACTIVE requires price > 0");
}
ProductMapper.applyUpdate(product, dto);
var saved = repo.save(product);
return ProductMapper.toResponse(saved);
}
public ProductResponseDto getById(Long id) {
return repo.findById(id)
.map(ProductMapper::toResponse)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
}
}
6) Controller¶
// src/main/java/com/example/product/ProductController.java
package com.example.product;
import com.example.product.dto.*;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@PostMapping
public ResponseEntity<ProductResponseDto> create(@Valid @RequestBody ProductCreateDto dto) {
return ResponseEntity.ok(service.create(dto));
}
@PatchMapping("/{id}")
public ResponseEntity<ProductResponseDto> update(
@PathVariable Long id,
@Valid @RequestBody ProductUpdateDto dto
) {
return ResponseEntity.ok(service.update(id, dto));
}
@GetMapping("/{id}")
public ResponseEntity<ProductResponseDto> get(@PathVariable Long id) {
return ResponseEntity.ok(service.getById(id));
}
}
7) Basic config for quick runs¶
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:h2:mem:demo;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
8) Minimal test (service)¶
// src/test/java/com/example/product/ProductServiceTest.java
package com.example.product;
import com.example.product.dto.ProductCreateDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ProductServiceTest {
@Autowired
ProductService service;
@Test
void create_ok() {
var dto = new ProductCreateDto("SKU-123", "Mouse", "Ergonomic", 2999L, "EUR", 10);
var res = service.create(dto);
assertNotNull(res.id());
assertEquals("SKU-123", res.sku());
assertEquals(10, res.stock());
}
}
π Key takeaways (tie back to your note)¶
- Use DTOs for input (
@Valid) and output; keep Entities internal to persistence. - Service layer = place for business rules (unique SKU, currency support, state transitions).
- Entity holds DB-focused constraints; DTO holds request/response validation.
- If you can wire:
Controller β DTO β Service β Repo/Entity β Response DTO, plusapplication.ymland one test, youβve hit the muscle-memory loop.