📚 Módulo 04: Backend - Camada de Serviços e Transações
✅ Pré-Requisitos deste Módulo
Confirme antes de começar:
./mvnw clean compile # deve retornar BUILD SUCCESS
Você deve ter concluído o Módulo 03 (todos os DTOs, Mappers e GlobalExceptionHandler criados).
Em Engenharia de Software, os controladores (Controllers) devem ser finos, encarregando-se apenas da recepção de dados HTTP e de seu respectivo retorno. O cérebro da nossa aplicação, contendo as regras e a orquestração do negócio, reside na Camada de Serviço (Service Layer).
Neste módulo, implementaremos os serviços de Produto e Pedido, focando na lógica de faturamento de pedidos e no uso da anotação @Transactional para garantir a consistência transacional do estoque.
🏛️ 1. O Padrão Service com Interfaces
A boa prática sugere o uso de Interfaces para definir os contratos dos nossos serviços. Isso traz dois benefícios essenciais:
- Desacoplamento: A API conhece apenas o contrato, facilitando a substituição da implementação concreta no futuro.
- Facilidade de Testes Unitários: Permite criar “mocks” (dublês de teste) da interface de forma simples para testar outras partes do código isoladamente.
Criaremos a estrutura de serviços no pacote br.com.tecloja.api.service e suas implementações no subpacote br.com.tecloja.api.service.impl.
📦 2. Serviço de Produtos
1. ProdutoService.java
package br.com.tecloja.api.service;
import br.com.tecloja.api.dto.ProdutoDTO;
import br.com.tecloja.api.dto.ProdutoFormDTO;
import java.util.List;
public interface ProdutoService {
List<ProdutoDTO> listarTodos();
ProdutoDTO buscarPorId(Long id);
List<ProdutoDTO> buscarPorCategoria(Long categoriaId);
List<ProdutoDTO> pesquisarPorNome(String busca);
ProdutoDTO criar(ProdutoFormDTO form);
ProdutoDTO atualizar(Long id, ProdutoFormDTO form);
void deletar(Long id);
}
2. ProdutoServiceImpl.java
package br.com.tecloja.api.service.impl;
import br.com.tecloja.api.dto.ProdutoDTO;
import br.com.tecloja.api.dto.ProdutoFormDTO;
import br.com.tecloja.api.exception.ResourceNotFoundException;
import br.com.tecloja.api.mapper.ProdutoMapper;
import br.com.tecloja.api.model.Categoria;
import br.com.tecloja.api.model.Produto;
import br.com.tecloja.api.repository.CategoriaRepository;
import br.com.tecloja.api.repository.ProdutoRepository;
import br.com.tecloja.api.service.ProdutoService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ProdutoServiceImpl implements ProdutoService {
private final ProdutoRepository produtoRepository;
private final CategoriaRepository categoriaRepository;
public ProdutoServiceImpl(ProdutoRepository produtoRepository, CategoriaRepository categoriaRepository) {
this.produtoRepository = produtoRepository;
this.categoriaRepository = categoriaRepository;
}
@Override
@Transactional(readOnly = true)
public List<ProdutoDTO> listarTodos() {
return produtoRepository.findAll().stream()
.filter(Produto::isAtivo)
.map(ProdutoMapper::toDTO)
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public ProdutoDTO buscarPorId(Long id) {
Produto produto = produtoRepository.findById(id)
.filter(Produto::isAtivo)
.orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
return ProdutoMapper.toDTO(produto);
}
@Override
@Transactional(readOnly = true)
public List<ProdutoDTO> buscarPorCategoria(Long categoriaId) {
return produtoRepository.findByCategoriaId(categoriaId).stream()
.filter(Produto::isAtivo)
.map(ProdutoMapper::toDTO)
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public List<ProdutoDTO> pesquisarPorNome(String busca) {
return produtoRepository.pesquisarPorNome(busca).stream()
.filter(Produto::isAtivo)
.map(ProdutoMapper::toDTO)
.collect(Collectors.toList());
}
@Override
@Transactional
public ProdutoDTO criar(ProdutoFormDTO form) {
Categoria categoria = categoriaRepository.findById(form.categoriaId())
.orElseThrow(() -> new ResourceNotFoundException("Categoria não encontrada com o ID: " + form.categoriaId()));
Produto produto = ProdutoMapper.toEntity(form, categoria);
Produto salvo = produtoRepository.save(produto);
return ProdutoMapper.toDTO(salvo);
}
@Override
@Transactional
public ProdutoDTO atualizar(Long id, ProdutoFormDTO form) {
Produto produto = produtoRepository.findById(id)
.filter(Produto::isAtivo)
.orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
Categoria categoria = categoriaRepository.findById(form.categoriaId())
.orElseThrow(() -> new ResourceNotFoundException("Categoria não encontrada com o ID: " + form.categoriaId()));
produto.setNome(form.nome());
produto.setDescricao(form.descricao());
produto.setPreco(form.preco());
produto.setEstoque(form.estoque());
produto.setCategoria(categoria);
Produto atualizado = produtoRepository.save(produto);
return ProdutoMapper.toDTO(atualizado);
}
@Override
@Transactional
public void deletar(Long id) {
Produto produto = produtoRepository.findById(id)
.filter(Produto::isAtivo)
.orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
// Em vez de deletar fisicamente, inativamos o produto (Soft Delete)
produto.setAtivo(false);
produtoRepository.save(produto);
}
}
[!NOTE] Boas Práticas de Mercado — Por que usamos Soft Delete (Exclusão Lógica)?
Em sistemas comerciais reais (especialmente e-commerces), a exclusão física (
Hard Delete) de registros é considerada uma prática extremamente arriscada. Se você excluir fisicamente um produto do banco de dados, todos os pedidos históricos de venda realizados no passado que referenciavam esse produto perderão a integridade referencial, gerando falhas graves no banco ou quebrando relatórios de faturamento.Ao adotar o Soft Delete, nós apenas alteramos a flag
ativoparafalse. O produto deixa de ser exibido na vitrine e nas pesquisas, mas permanece intacto no histórico do banco, preservando com absoluta segurança a rastreabilidade e integridade contábil das vendas passadas.
🛒 3. Serviço de Pedidos e Controle de Estoque Transacional
O checkout é a regra de negócio mais complexa e importante da TecLoja. Ao faturar um pedido, o sistema precisa:
- Validar se o cliente existe.
- Para cada item do pedido, validar se o produto existe no catálogo.
- Verificar a disponibilidade de estoque: se a quantidade solicitada for maior que o saldo em estoque, o faturamento deve ser cancelado.
- Reduzir a quantidade vendida do estoque do produto.
- Gravar os dados históricos do produto (preço unitário cobrado no instante da compra) no
ItemPedido. - Persistir as informações estruturadas.
🧠 Por que utilizar @Transactional?
A anotação @Transactional do Spring informa que todos os comandos executados no método fazem parte de uma única transação de banco de dados (Princípio ACID).
Se o cliente tentar comprar 3 itens e um deles falhar por estoque insuficiente no meio do processo, toda a operação sofrerá um “Rollback”. Nenhum produto terá seu estoque debitado e nenhuma linha de pedido será escrita no banco, mantendo o banco de dados livre de inconsistências.
sequenceDiagram
participant API as PedidoController
participant Service as PedidoServiceImpl
participant DB as Banco (PostgreSQL)
API->>Service: realizarPedido()
Note over Service: @Transactional inicia
Service->>DB: BEGIN
loop Verificando Itens
Service->>DB: Buscar Estoque
alt Estoque Insuficiente
Service-->>API: throws BusinessException
Service->>DB: ROLLBACK (Automático)
else Estoque Ok
Service->>DB: UPDATE (Baixa de Estoque)
end
end
Service->>DB: INSERT Pedido / Itens
Service->>DB: COMMIT (Automático)
Service-->>API: PedidoDTO (Sucesso)
1. PedidoService.java
package br.com.tecloja.api.service;
import br.com.tecloja.api.dto.PedidoDTO;
import br.com.tecloja.api.dto.PedidoFormDTO;
import java.util.List;
public interface PedidoService {
PedidoDTO realizarPedido(PedidoFormDTO form);
List<PedidoDTO> listarPedidosPorCliente(Long clienteId);
}
2. PedidoServiceImpl.java
package br.com.tecloja.api.service.impl;
import br.com.tecloja.api.dto.ItemPedidoFormDTO;
import br.com.tecloja.api.dto.PedidoDTO;
import br.com.tecloja.api.dto.PedidoFormDTO;
import br.com.tecloja.api.exception.BusinessException;
import br.com.tecloja.api.exception.ResourceNotFoundException;
import br.com.tecloja.api.mapper.PedidoMapper;
import br.com.tecloja.api.model.*;
import br.com.tecloja.api.repository.*;
import br.com.tecloja.api.service.PedidoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class PedidoServiceImpl implements PedidoService {
private final PedidoRepository pedidoRepository;
private final ClienteRepository clienteRepository;
private final ProdutoRepository produtoRepository;
public PedidoServiceImpl(PedidoRepository pedidoRepository, ClienteRepository clienteRepository,
ProdutoRepository produtoRepository) {
this.pedidoRepository = pedidoRepository;
this.clienteRepository = clienteRepository;
this.produtoRepository = produtoRepository;
}
@Override
@Transactional // Inicia uma transação ativa. Qualquer erro causará rollback automático do estoque!
public PedidoDTO realizarPedido(PedidoFormDTO form) {
log.info("Iniciando faturamento de pedido para o cliente ID: {}", form.clienteId());
// 1. Validar Cliente
Cliente cliente = clienteRepository.findById(form.clienteId())
.orElseThrow(() -> new ResourceNotFoundException("Cliente não encontrado com o ID: " + form.clienteId()));
// 2. Instanciar Novo Pedido
Pedido pedido = new Pedido();
pedido.setCliente(cliente);
pedido.setStatus("PAGO"); // Venda simplificada com pagamento imediato aprovado
pedido.setDataPedido(LocalDateTime.now());
// 3. Processar Itens e Dar Baixa de Estoque
for (ItemPedidoFormDTO itemForm : form.itens()) {
Produto produto = produtoRepository.findById(itemForm.produtoId())
.orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado com o ID: " + itemForm.produtoId()));
// Regra de Negócio: produto inativo não pode ser vendido (Soft Delete)
if (!produto.isAtivo()) {
log.warn("Falha no checkout: tentativa de comprar produto inativo '{}' (ID: {}).", produto.getNome(), produto.getId());
throw new BusinessException(String.format(
"O produto '%s' não está mais disponível para venda no catálogo.",
produto.getNome()
));
}
// Regra Crítica de Banco de Dados/Negócio: Validação de Estoque
if (produto.getEstoque() < itemForm.quantidade()) {
log.warn("Falha no checkout: estoque insuficiente para o produto '{}' (ID: {}). Solicitado: {}, Disponível: {}",
produto.getNome(), produto.getId(), itemForm.quantidade(), produto.getEstoque());
throw new BusinessException(String.format(
"Estoque insuficiente para o produto '%s'. Disponível: %d, Solicitado: %d.",
produto.getNome(), produto.getEstoque(), itemForm.quantidade()
));
}
// Reduzir estoque físico do produto e salvar
produto.setEstoque(produto.getEstoque() - itemForm.quantidade());
produtoRepository.save(produto);
// Criar ItemPedido com cópia de preço unitário histórico
ItemPedido item = new ItemPedido();
item.setProduto(produto);
item.setQuantidade(itemForm.quantidade());
item.setPrecoUnitario(produto.getPreco()); // Copia o preço ATUAL do produto
// Vincula o item ao pedido de forma bidirecional
pedido.adicionarItem(item);
}
// 4. Salvar Pedido e Itens em cascata
Pedido pedidoSalvo = pedidoRepository.save(pedido);
log.info("Pedido ID: {} faturado com sucesso para o cliente ID: {}.", pedidoSalvo.getId(), cliente.getId());
// 5. Retornar DTO faturado
return PedidoMapper.toDTO(pedidoSalvo);
}
@Override
@Transactional(readOnly = true)
public List<PedidoDTO> listarPedidosPorCliente(Long clienteId) {
// Valida se o cliente existe antes
if (!clienteRepository.existsById(clienteId)) {
throw new ResourceNotFoundException("Cliente não encontrado com o ID: " + clienteId);
}
return pedidoRepository.findPedidosCompletosPorCliente(clienteId).stream()
.map(PedidoMapper::toDTO)
.collect(Collectors.toList());
}
}
[!NOTE] Boas Práticas de Mercado — Por que usamos SLF4J / Logging Estruturado?
Em sistemas reais em produção, a ausência de logs torna impossível depurar erros ou rastrear o comportamento dos usuários em tempo real (observabilidade). Usar comandos simples como
System.out.printlné considerado uma má prática gravíssima em Java corporativo, pois essas impressões travam a thread de execução (I/O síncrono) e não possuem níveis de severidade.A biblioteca SLF4J (Simple Logging Facade for Java) com o Logback (facilmente integrado via Spring Boot) permite registrar eventos com diferentes níveis de severidade:
log.info(...): Para rastrear o fluxo feliz do sistema (ex: faturamento concluído com sucesso).log.warn(...): Para alertar sobre possíveis problemas ou comportamentos inesperados que não chegam a derrubar a API (ex: tentativa de compra com estoque insuficiente).log.error(...): Para capturar falhas críticas do sistema.Além disso, note a sintaxe parametrizada
log.info("Mensagem: {}", valor). Essa estrutura evita a concatenação de strings desnecessária em memória antes do log ser de fato filtrado, otimizando drasticamente o desempenho de APIs de alto tráfego.
🔍 Checkpoint
Após criar os serviços, compile e execute os testes unitários:
./mvnw clean test
Resultado esperado: BUILD SUCCESS com todos os testes passando (Tests run: X, Failures: 0, Errors: 0).
Se ainda não criou o arquivo de teste, pode verificar a compilação simples com ./mvnw clean compile. Os testes completos serão adicionados no Módulo 09.
⚠️ Erros Comuns
| Sintoma | Causa | Solução |
|---|---|---|
No qualifying bean of type 'ProdutoService' |
Interface sem implementação detectada | Confirme que ProdutoServiceImpl tem a anotação @Service |
Could not commit JPA transaction |
Exceção lançada dentro de @Transactional |
Comportamento correto — o Spring faz rollback automaticamente. Verifique a mensagem de erro no JSON de resposta |
LazyInitializationException no Mapper |
getCategoria() chamado fora de sessão ativa |
Confirme que listarTodos() tem @Transactional(readOnly = true) |
| Estoque negativo após pedido | @Transactional ausente no realizarPedido |
Confirme a anotação no método realizarPedido em PedidoServiceImpl |
🏁 Conclusão e Próximos Passos
A inteligência da TecLoja está concluída! Ao programarmos as regras transacionais de faturamento de pedidos, os alunos passam a entender a importância do controle de concorrência e integridade referencial em banco de dados utilizando JPA e transações ativas.
No Módulo 05, exporemos essa lógica de negócio para a internet. Desenvolveremos controladores REST (@RestController), configuraremos as permissões de CORS para receber requisições do frontend hospedado na Netlify e blindaremos a API com segurança avançada por meio do Spring Security e JWT.