Prática Completa: Desenvolvimento do Projeto OrderFlow
Desenvolvimento de projeto é o processo de transformar design em código — avançando de forma modular, com cada etapa verificável e integração contínua para evitar acúmulos.
1. O Que Você Vai Aprender
- Módulo de Usuário: Registro / Login, Emissão de JWT / Controle de Acesso Baseado em Papéis
- Módulo de Produto: CRUD / Cache Redis / Cache de Segundo Nível Caffeine
- Módulo de Pedido: Realização de Pedido / Cancelamento / Cancelamento Automático por Timeout / Transações de Dedução de Estoque
- Módulo de Pagamento: Pagamentos Simulados / Tratamento de Callback / Garantia de Idempotência
- Funcionalidades Globais: Tratamento de Exceções / Log de Requisições / Validação de Parâmetros / Documentação de API (SpringDoc)
2. Uma História Real de um Desenvolvedor Full-Stack
(1) Dor: O Gap Entre Design e Código
Alice completou o design do sistema do OrderFlow, mas se sente sobrecarregada pelo volume de código — 5 tabelas, 20 APIs e 4 módulos. Ela deve começar escrevendo o código do banco de dados ou o código da API? Como os módulos devem depender uns dos outros? Como pode garantir que cada etapa execute e passe na validação?
(2) Soluções para Desenvolvimento Modular
Proceder passo a passo por módulo, com cada módulo verificável independentemente:
graph TD
A["Módulo de Usuário<br/>Auth + JWT"] --> B["Módulo de Produto<br/>CRUD + Cache"]
B --> C["Módulo de Pedido<br/>Transação + Agendamento"]
C --> D["Módulo de Pagamento<br/>Callback + Idempotente"]
D --> E["Transversal<br/>Exceção + Logging + Doc"]
(3) Resultado
Alice desenvolveu o projeto módulo por módulo, executando testes para verificar cada módulo conforme era completado. Ela finalizou toda a base de código em duas semanas, evitando os problemas de "big bang" associados a uma integração única.
3. Módulo de Usuário
(1) ▶ Exemplo: Entidade e Repositório de Usuário
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, name = "password_hash")
private String passwordHash;
@Column(nullable = false, length = 20)
private String role = "CUSTOMER";
@CreationTimestamp
private Instant createdAt;
protected User() {}
public User(String username, String email, String passwordHash, String role) {
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
}
// getters
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Registro e Login
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtEncoder jwtEncoder;
public AuthService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtEncoder jwtEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtEncoder = jwtEncoder;
}
@Transactional
public UserResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new BusinessException("USERNAME_EXISTS", "Nome de usuário já em uso");
}
User user = new User(
request.username(),
request.email(),
passwordEncoder.encode(request.password()),
"CUSTOMER"
);
return UserResponse.from(userRepository.save(user));
}
public Map<String, String> login(LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow").subject(auth.getName())
.issuedAt(now).expiresAt(now.plus(2, ChronoUnit.HOURS))
.claim("role", extractRole(auth))
.build();
String token = jwtEncoder
.encode(JwtEncoderParameters.from(claims)).getTokenValue();
return Map.of("accessToken", token);
}
}
Saída:
// Execução bem-sucedida
4. Módulo de Produto
(1) ▶ Exemplo: ProductService com Cache
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductSearchRepository searchRepository; // Redis
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
return ProductResponse.from(product);
}
@Cacheable(value = "product-list",
key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
public PagedResponse<ProductResponse> searchProducts(
String keyword, Pageable pageable) {
Page<Product> page = productRepository
.findByNameContaining(keyword, pageable);
return PagedResponse.from(page.map(ProductResponse::from));
}
@CacheEvict(value = "product-list", allEntries = true)
@CachePut(value = "products", key = "#result.id()")
public ProductResponse createProduct(CreateProductRequest request) {
Product product = new Product(
request.name(), request.sku(),
request.price(), request.stock(), request.category());
return ProductResponse.from(productRepository.save(product));
}
@CachePut(value = "products", key = "#result.id()")
@CacheEvict(value = "product-list", allEntries = true)
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
product.setName(request.name());
product.setPrice(request.price());
product.setStock(request.stock());
return ProductResponse.from(productRepository.save(product));
}
@Caching(evict = {
@CacheEvict(value = "products", key = "#id"),
@CacheEvict(value = "product-list", allEntries = true)
})
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
Saída:
// Execução bem-sucedida
5. Módulo de Pedido
(1) ▶ Exemplo: Transação de Pedido no OrderService
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
private final OrderEventPublisher eventPublisher;
@Transactional(rollbackFor = Exception.class)
public OrderResponse createOrder(String username, CreateOrderRequest request) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", 0L));
Product product = productRepository.findById(request.productId())
.orElseThrow(() -> new ResourceNotFoundException("Product", request.productId()));
if (product.getStock() < request.quantity()) {
throw new InsufficientStockException(
product.getId(), product.getStock(), request.quantity());
}
product.deductStock(request.quantity());
Order order = new Order(user, product, request.quantity());
Order saved = orderRepository.save(order);
eventPublisher.publishOrderCreated(saved.getId());
return OrderResponse.from(saved);
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String username, Long orderId) {
Order order = orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
if (!"PENDING".equals(order.getStatus())) {
throw new OrderStateException(orderId, order.getStatus(), "CANCELLED");
}
order.getItems().forEach(item ->
item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
eventPublisher.publishOrderCancelled(orderId);
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
@Transactional
public void cancelExpiredOrders() {
List<Order> expired = orderRepository
.findByStatusAndCreatedAtBefore("PENDING",
Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> {
order.getItems().forEach(item ->
item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
});
if (!expired.isEmpty()) {
log.info("Auto-cancelled {} expired orders", expired.size());
}
}
}
Saída:
// Execução bem-sucedida
6. Módulo de Pagamento
(1) ▶ Exemplo: PaymentService com Idempotência
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final OrderRepository orderRepository;
private final StringRedisTemplate redis;
@Transactional(rollbackFor = Exception.class)
public PaymentResponse initiatePayment(String username, CreatePaymentRequest request) {
Order order = orderRepository.findById(request.orderId())
.orElseThrow(() -> new ResourceNotFoundException("Order", request.orderId()));
if (!"PENDING".equals(order.getStatus())) {
throw new OrderStateException(order.getId(), order.getStatus(), "PAYMENT");
}
// Verificação de idempotência: mesmo transaction_id não deve criar pagamento duplicado
String idempotencyKey = "payment:idempotent:" + request.orderId();
Boolean isNew = redis.opsForValue()
.setIfAbsent(idempotencyKey, "1", Duration.ofMinutes(10));
if (Boolean.FALSE.equals(isNew)) {
throw new BusinessException("DUPLICATE_PAYMENT",
"Pagamento já iniciado para o pedido " + request.orderId());
}
Payment payment = new Payment(order, request.method(), order.getTotalAmount());
payment.setTransactionId(generateTransactionId());
return PaymentResponse.from(paymentRepository.save(payment));
}
@Transactional(rollbackFor = Exception.class)
public void handlePaymentCallback(String transactionId, String status) {
Payment payment = paymentRepository.findByTransactionId(transactionId)
.orElseThrow(() -> new ResourceNotFoundException("Payment", 0L));
if ("COMPLETED".equals(status) && "PENDING".equals(payment.getStatus())) {
payment.setStatus("COMPLETED");
payment.setPaidAt(Instant.now());
payment.getOrder().setStatus("PAID");
} else if ("FAILED".equals(status)) {
payment.setStatus("FAILED");
}
}
private String generateTransactionId() {
return "TXN-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
}
}
Saída:
// Execução bem-sucedida
7. Funcionalidades Globais
(1) ▶ Exemplo: Configuração de Documentação de API SpringDoc
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI orderFlowOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("OrderFlow API")
.version("1.0.0")
.description("API do sistema de gerenciamento de pedidos de e-commerce"))
.addSecurityItem(new SecurityRequirement().addList("Bearer Auth"))
.schemaRequirement("Bearer Auth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));
}
}
Saída:
// Execução bem-sucedida
| Funcionalidade Global | Implementação | Escopo |
|---|---|---|
| Tratamento de Exceções | @RestControllerAdvice | Formato de Erro Padronizado Global |
| Log de Requisições | HandlerInterceptor | Log de Todas as Requisições de Entrada |
| Validação de Parâmetros | Bean Validation | Todos os DTOs de Requisição |
| Documentação de API | SpringDoc OpenAPI | Geração Automática do Swagger UI |
| Autenticação de Segurança | Spring Security + JWT | Todas as Interfaces Protegidas |
8. Exemplo Completo: Estrutura Completa do Projeto OrderFlow
orderflow-service/
├── src/main/java/com/orderflow/
│ ├── OrderFlowApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java # JWT + Auth por papel
│ │ ├── CacheConfig.java # Caffeine + Redis
│ │ ├── AsyncConfig.java # Pool de threads + Agendamento
│ │ └── OpenApiConfig.java # Documentação de API
│ ├── controller/
│ │ ├── AuthController.java # Login / Registro
│ │ ├── ProductController.java # CRUD de Produtos
│ │ ├── OrderController.java # Gerenciamento de pedidos
│ │ └── PaymentController.java # Processamento de pagamentos
│ ├── service/
│ │ ├── AuthService.java
│ │ ├── ProductService.java
│ │ ├── OrderService.java
│ │ ├── PaymentService.java
│ │ └── NotificationService.java # Notificações @Async
│ ├── repository/
│ │ ├── UserRepository.java
│ │ ├── ProductRepository.java
│ │ ├── OrderRepository.java
│ │ └── PaymentRepository.java
│ ├── model/
│ │ ├── User.java
│ │ ├── Product.java
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── Payment.java
│ ├── dto/
│ │ ├── request/ # CreateOrderRequest, etc.
│ │ └── response/ # OrderResponse, etc.
│ ├── exception/
│ │ ├── BusinessException.java
│ │ ├── ResourceNotFoundException.java
│ │ ├── GlobalExceptionHandler.java
│ │ └── ErrorResponse.java
│ ├── security/
│ │ ├── JwtConfig.java # Par de chaves RSA
│ │ ├── CustomUserDetailsService.java
│ │ └── OrderSecurity.java # Auxiliar @PreAuthorize
│ └── metrics/
│ └── OrderMetrics.java # Métricas personalizadas Micrometer
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── db/migration/ # Scripts Flyway
│ ├── V1__init_schema.sql
│ └── V2__add_indexes.sql
├── Dockerfile
├── docker-compose.yml
└── pom.xml
❓ Perguntas Frequentes
@Schema aos DTOs para fornecer explicações adicionais.V{version}__{description}.sql, como V1__init_schema.sql e V2__add_indexes.sql. Os números de versão devem ser sequenciais e scripts já executados não devem ser modificados.📖 Resumo
- Módulo de Usuário: Hash de senha BCrypt, emissão JWT, controle de acesso baseado em papéis
- Módulo de Produto: @Cacheable para cache de dois níveis, @CachePut para atualizações síncronas, @CacheEvict para invalidação
- Módulo de Pedido: @Transactional para dedução de estoque ao fazer pedido, @Scheduled para cancelamento automático por timeout e notificações de eventos Pub/Sub
- Módulo de Pagamento: Idempotência com Redis SETNX, garantias de máquina de estados, tratamento de callback
- Funcionalidades Globais: @RestControllerAdvice para tratamento de exceções unificado, documentação de API SpringDoc e Bean Validation
📝 Exercícios
-
Exercício Básico (Dificuldade: ⭐): Implemente código completo para registro/login de usuários e operações CRUD de produtos, garantindo que autenticação JWT e cache estejam habilitados.
-
Exercício Avançado (Dificuldade: ⭐⭐): Implemente o módulo de pedidos (fazer pedido, cancelar pedido e cancelamento automático por timeout) e o módulo de pagamentos (iniciar pagamento e callbacks idempotentes) e use Postman para realizar testes de processo de negócio end-to-end.
-
Desafio (Dificuldade: ⭐⭐⭐): Adicione documentação de API SpringDoc ao OrderFlow e configure o Swagger UI para testes interativos; adicione scripts de migração de banco de dados Flyway; escreva testes de integração para o fluxo principal usando TestContainers.



