404 Not Found

404 Not Found


nginx

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


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:

100%
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

JAVA
@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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Registro e Login

JAVA
@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:

TEXT
// Execução bem-sucedida

4. Módulo de Produto

(1) ▶ Exemplo: ProductService com Cache

JAVA
@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:

TEXT
// Execução bem-sucedida

5. Módulo de Pedido

(1) ▶ Exemplo: Transação de Pedido no OrderService

JAVA
@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:

TEXT
// Execução bem-sucedida

6. Módulo de Pagamento

(1) ▶ Exemplo: PaymentService com Idempotência

JAVA
@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:

TEXT
// Execução bem-sucedida

7. Funcionalidades Globais

(1) ▶ Exemplo: Configuração de Documentação de API SpringDoc

JAVA
@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:

TEXT
// 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

TEXT
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

P Devo escrever a Entity primeiro ou o Controller?
R Recomendamos uma abordagem bottom-up: Entity → Repository → Service → Controller. Primeiro, defina o modelo de dados; depois implemente o acesso a dados; em seguida, escreva a lógica de negócio; e por fim, implemente a camada de API. Desta forma, cada camada pode ser testada independentemente.
P Como garantimos a idempotência dos callbacks de pagamento?
R 1) Redis SETNX garante que um pagamento seja iniciado apenas uma vez por pedido; 2) Restrição única no banco de dados (transaction_id); 3) Máquina de estados do pagamento (PENDING → COMPLETED é irreversível); 4) Registrar todos os eventos de callback para fins de conciliação.
P Como o cache de dois níveis garante consistência?
R Durante uma atualização: 1) Atualizar o banco de dados primeiro; 2) Deletar o cache Redis; 3) Notificar todas as instâncias via Redis Pub/Sub para limpar seus caches Caffeine locais. Para leituras: Primeiro verificar Caffeine → depois verificar Redis → finalmente verificar o banco de dados.
P Como a documentação da API e o código são mantidos em sincronia?
R O SpringDoc escaneia automaticamente as anotações do Controller para gerar documentação OpenAPI; quando o código muda, a documentação é atualizada automaticamente. No entanto, você precisa adicionar a anotação @Schema aos DTOs para fornecer explicações adicionais.
P Quais são as convenções de nomenclatura dos scripts de migração do Flyway?
R 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.
P Como organizar o desenvolvimento colaborativo entre múltiplas pessoas?
R 1) Criar branches Git por módulo (feature/user, feature/order); 2) Integrar mudanças na branch develop diariamente; 3) Fazer merge após code review; 4) Executar testes automaticamente via CI. A chave é garantir que cada módulo seja autocontido e testável.

📖 Resumo


📝 Exercícios

  1. 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%