# Entrevista Spring Boot: Propagação de Transações > Domine a propagação de transações no Spring Boot: REQUIRED, REQUIRES_NEW, NESTED e mais. 12 perguntas de entrevista com código e armadilhas comuns. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- A propagação de transações representa um conceito fundamental no Spring Boot, regularmente avaliado em entrevistas técnicas. Compreender como as transações interagem entre métodos anotados com `@Transactional` ajuda a evitar bugs sutis em produção e permite projetar arquiteturas robustas. > **Dica de preparação** > > Os entrevistadores avaliam a capacidade de escolher o nível de propagação correto conforme o contexto de negócio. Saber explicar por que REQUIRES_NEW em vez de REQUIRED em um caso específico faz a diferença. ## Fundamentos da propagação de transações ### Pergunta 1: O que é propagação de transações no Spring? A propagação define o comportamento de um método transacional quando é chamado dentro do contexto de uma transação existente. Ela responde à pergunta: "O que acontece quando um método `@Transactional` chama outro método também anotado?" ```java // OrderService.java // Demonstrando o conceito de propagação @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Transação pai - inicia uma nova transação @Transactional public void createOrder(OrderRequest request) { // Salva o pedido na transação atual Order order = orderRepository.save(new Order(request)); // Chamada para outro método @Transactional // A propagação determina: mesma transação ou nova? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Propagação padrão: REQUIRED // Junta-se à transação existente de createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Executa na MESMA transação que createOrder() // Se este método falhar, o pedido também é revertido } } ``` O Spring fornece sete níveis de propagação, cada um adequado a necessidades de negócio específicas. A escolha impacta diretamente a consistência dos dados e o desempenho. ### Pergunta 2: Descreva o comportamento de REQUIRED (propagação padrão) REQUIRED é a propagação padrão. Se uma transação existir, o método junta-se a ela. Caso contrário, uma nova transação é criada. É o comportamento mais comum e intuitivo. ```java // UserService.java // REQUIRED: comportamento padrão @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Inicia uma transação se nenhuma existir User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // A auditoria junta-se à mesma transação auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Junta-se à transação de updateUser() // Commit ou rollback juntos auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` O diagrama abaixo ilustra o fluxo transacional: ```text updateUser() inicia TX-1 ├── save(user) → TX-1 └── logUpdate() → junta-se a TX-1 (REQUIRED) └── save(audit) → TX-1 Se logUpdate() falhar → rollback TX-1 → user E audit cancelados ``` ### Pergunta 3: Quando usar REQUIRES_NEW em vez de REQUIRED? REQUIRES_NEW suspende a transação existente e cria uma nova transação independente. Útil quando uma operação precisa ser persistida independentemente do resultado da transação pai. ```java // PaymentService.java // REQUIRES_NEW: transação independente @Service public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentAuditService auditService; @Transactional public void processPayment(Long orderId, BigDecimal amount) { Payment payment = new Payment(orderId, amount); paymentRepository.save(payment); // Auditoria DEVE ser persistida mesmo se o pagamento falhar depois auditService.logPaymentAttempt(orderId, amount); // Simulando um erro após a auditoria if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: faz commit independente da transação pai @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Nova transação TX-2 criada // TX-1 (processPayment) está suspensa auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 faz commit aqui, independentemente de TX-1 } } ``` O fluxo transacional com REQUIRES_NEW: ```text processPayment() inicia TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 SUSPENSA │ └── inicia TX-2 → nova transação │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit persistido │ └── TX-1 RETOMA └── throw InvalidAmountException └── ROLLBACK TX-1 → pagamento cancelado, mas audit preservado ``` > **Cuidado com deadlocks** > > REQUIRES_NEW pode causar deadlocks se a nova transação acessar os mesmos recursos bloqueados pela transação suspensa. Evite usar REQUIRES_NEW para modificar as mesmas tabelas que a transação pai. ### Pergunta 4: Explique a propagação NESTED e como ela difere de REQUIRES_NEW NESTED cria um savepoint dentro da transação atual. Se o método falhar, apenas as modificações desde o savepoint são revertidas, e não toda a transação pai. ```java // BatchProcessingService.java // NESTED: savepoint dentro da transação pai @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // Cada item é processado com um savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback apenas deste item, não do batch inteiro result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit dos itens bem-sucedidos } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: cria um savepoint, rollback parcial possível @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Validação de negócio if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback até o savepoint → apenas este item } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Comparação NESTED vs REQUIRES_NEW: ```text NESTED: ├── Usa um savepoint dentro da TX pai ├── Em caso de falha → rollback ao savepoint ├── Se a TX pai fizer rollback → NESTED também reverte └── Mais performático (sem nova conexão) REQUIRES_NEW: ├── Cria uma transação completamente independente ├── Em caso de falha → rollback apenas da TX filha ├── Se a TX pai fizer rollback → TX filha JÁ FEZ COMMIT └── Requer uma nova conexão ``` > **Suporte NESTED** > > A propagação NESTED requer suporte a savepoints JDBC. A maioria dos bancos modernos (PostgreSQL, MySQL, Oracle) suporta isso. Verifique a compatibilidade antes de usar. ## Tipos de propagação avançados ### Pergunta 5: Quando usar SUPPORTS e NOT_SUPPORTED? SUPPORTS executa dentro da transação existente, se houver, caso contrário sem transação. NOT_SUPPORTED suspende qualquer transação existente e executa sem transação. ```java // ReportingService.java // SUPPORTS: transação opcional @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: funciona com ou sem transação // Útil para leituras que não precisam de garantias transacionais @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Se chamado de um método @Transactional → usa sua TX // Se chamado diretamente → sem transação (leitura OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: nunca dentro de uma transação // Evita bloquear a TX durante uma chamada externa lenta @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX pai suspensa durante a chamada apiClient.send(message); // Chamada HTTP potencialmente lenta // TX pai retoma depois } } ``` ```java // OrderService.java // Exemplo de uso combinado @Service public class OrderService { private final OrderRepository orderRepository; private final ReportingService reportingService; private final ExternalNotificationService notificationService; @Transactional public void completeOrder(Long orderId) { Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus("COMPLETED"); orderRepository.save(order); // Geração do relatório na mesma TX (SUPPORTS) Report report = reportingService.generateReport(orderId); // Notificação externa FORA da transação (NOT_SUPPORTED) // Evita timeout da TX se a API externa estiver lenta notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Pergunta 6: Explique MANDATORY e NEVER MANDATORY exige uma transação existente e lança uma exceção caso contrário. NEVER exige a ausência de transação e lança uma exceção se uma existir. ```java // AuditService.java // MANDATORY: deve ser chamado de dentro de uma transação @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: recusa-se a executar sem uma transação existente // Garante que a auditoria é sempre atômica com a operação auditada @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Se chamado sem transação → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: nunca deve estar em uma transação @Service public class CacheService { private final CacheManager cacheManager; // NEVER: o cache não deve participar de transações // Evita inconsistências entre cache e BD após rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Se chamado de um @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Uso correto de MANDATORY @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Operação sensível... updatePassword(userId, newPassword); // A auditoria DEVE estar na mesma transação // MANDATORY impõe esta restrição arquitetural auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // ERRO: chamada direta sem transação public void badUsage() { // Lança IllegalTransactionStateException porque não há TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Armadilhas comuns em entrevistas ### Pergunta 7: Por que @Transactional não funciona em chamadas internas? Uma das armadilhas mais comuns. Chamadas internas a métodos (self-invocation) ignoram o proxy do Spring, desativando o gerenciamento transacional. ```java // BrokenService.java // ERRO CLÁSSICO: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // ARMADILHA: chamada interna → ignora o proxy // @Transactional em processItem() é IGNORADO processItem(id); } } @Transactional public void processItem(Long itemId) { // Esta transação NUNCA é criada em chamadas internas Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Soluções para evitar esta armadilha: ```java // Solução 1: Auto-injeção @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Auto-injeção public void processItems(List itemIds) { for (Long id : itemIds) { // Chamada via proxy → @Transactional funciona self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Transação corretamente criada Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Solução 2: Separar em dois serviços (recomendado) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Chamada para outro bean → o proxy funciona processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Transação corretamente gerenciada Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Pergunta 8: Como tratar exceções e rollback? Por padrão, o Spring só faz rollback em RuntimeException e Error. Exceções checked NÃO disparam rollback automático. ```java // TransactionRollbackDemo.java // Comportamento de rollback conforme o tipo de exceção @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → ROLLBACK automático @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → SEM rollback por padrão @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // Faz COMMIT mesmo assim! } // Forçar rollback em checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK graças a rollbackFor } // Excluir uma RuntimeException do rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT apesar da exceção } } ``` Configuração recomendada para casos de negócio: ```java // BaseTransactionalService.java // Configuração transacional consistente @Service public abstract class BaseTransactionalService { // Rollback em todas as exceções (checked e unchecked) @Transactional(rollbackFor = Exception.class) protected void executeInTransaction(Runnable operation) { operation.run(); } } // PaymentService.java @Service public class PaymentService extends BaseTransactionalService { private final PaymentRepository paymentRepository; @Transactional( rollbackFor = Exception.class, noRollbackFor = InsufficientFundsException.class ) public PaymentResult processPayment(PaymentRequest request) { Payment payment = new Payment(request); paymentRepository.save(payment); if (request.getAmount().compareTo(getBalance()) > 0) { // NÃO faz rollback - quer manter o registro da tentativa throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Pergunta 9: Como o isolamento de transações funciona com a propagação? Isolamento e propagação são complementares. O isolamento determina a visibilidade dos dados entre transações concorrentes. ```java // IsolationDemo.java // Combinando isolamento + propagação @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: vê dados commitados por outras transações @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Pode ver valores diferentes se relido durante a transação return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: garante a mesma leitura durante a transação @Transactional( isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED ) public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); // Mesmo que outra transação modifique 'from' nesse meio tempo, // sempre vemos o valor inicial (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: isolamento máximo, sem concorrência @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Bloqueia qualquer outra transação sobre estes dados // Usar com moderação - impacto no desempenho Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Tabela resumo dos níveis de isolamento: ```text | Isolamento | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Possível | Possível | Possível| | READ_COMMITTED | Não | Possível | Possível| | REPEATABLE_READ | Não | Não | Possível| | SERIALIZABLE | Não | Não | Não | ``` > **Impacto no desempenho** > > Quanto mais rigoroso o isolamento, mais o desempenho pode ser afetado por locks. SERIALIZABLE pode causar contenção significativa em produção de alto tráfego. ## Padrões avançados ### Pergunta 10: Como implementar o padrão "transactional outbox"? O padrão outbox garante consistência entre as modificações no banco de dados e o envio de mensagens/eventos, mesmo em caso de falha. ```java // OutboxService.java // Padrão Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Salva o evento na mesma transação que a entidade de negócio @Transactional(propagation = Propagation.MANDATORY) public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) { OutboxEvent event = OutboxEvent.builder() .aggregateType(aggregateType) .aggregateId(aggregateId) .eventType(eventType) .payload(payload) .status("PENDING") .createdAt(Instant.now()) .build(); outboxRepository.save(event); } } @Service public class OrderService { private final OrderRepository orderRepository; private final OutboxService outboxService; @Transactional public Order createOrder(OrderRequest request) { // Criar o pedido Order order = new Order(request); orderRepository.save(order); // Evento outbox na MESMA transação (MANDATORY) // Se o commit der certo → ambos são persistidos // Se houver rollback → nenhum é persistido outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Processo separado que publica eventos @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Transação independente para cada publicação @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List events = outboxRepository .findByStatusOrderByCreatedAt("PENDING"); for (OutboxEvent event : events) { try { messageBroker.publish(event.getEventType(), event.getPayload()); event.setStatus("PUBLISHED"); event.setPublishedAt(Instant.now()); } catch (Exception e) { event.setStatus("FAILED"); event.setError(e.getMessage()); } outboxRepository.save(event); } } } ``` ### Pergunta 11: Como testar diferentes comportamentos de propagação? Testes de propagação requerem atenção especial para verificar o comportamento transacional esperado. ```java // TransactionPropagationTest.java // Testando comportamentos de propagação @SpringBootTest @Transactional class TransactionPropagationTest { @Autowired private OrderService orderService; @Autowired private PaymentService paymentService; @Autowired private TestEntityManager entityManager; @Test void required_shouldShareTransaction() { // Given Order order = orderService.createOrder(new OrderRequest()); // When - pagamento na mesma transação (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - ambos visíveis antes do commit entityManager.flush(); assertThat(entityManager.find(Order.class, order.getId())).isNotNull(); assertThat(entityManager.find(Payment.class, order.getId())).isNotNull(); } @Test void requiresNew_shouldCommitIndependently() { // Given Long orderId = null; try { // When - audit em REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Transação principal faz rollback } // Then - audit (REQUIRES_NEW) ainda está commitado assertThat(findAuditLog(orderId)).isNotNull(); // Mas o pedido foi revertido assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - nenhuma transação ativa // When/Then - deve lançar uma exceção assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Teste de integração com rollback e commit reais @SpringBootTest class PropagationIntegrationTest { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Autowired private AuditLogRepository auditLogRepository; @Test void nested_shouldRollbackOnlyNestedOnFailure() { // Given int initialCount = orderRepository.findAll().size(); // When - processamento batch com NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - apenas itens válidos são persistidos assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Pergunta 12: Quais são as melhores práticas para configuração de transações? Uma configuração transacional consistente em nível de projeto evita surpresas e facilita a manutenção. ```java // TransactionConfig.java // Configuração transacional centralizada @Configuration @EnableTransactionManagement public class TransactionConfig { // Timeout padrão para todas as transações @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 segundos máximo por transação return tm; } } // BaseService.java // Anotações padrão para serviços @Service @Transactional( readOnly = true, // Apenas leitura por padrão rollbackFor = Exception.class // Rollback em qualquer exceção ) public abstract class BaseService { // Métodos de leitura herdam readOnly = true } // OrderService.java // Serviço com configuração consistente @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Herda readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override para escritas @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Configuração explícita para casos críticos @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Operação com isolamento máximo e timeout curto } } ``` Checklist de melhores práticas: ```text Configuração de Transação - Checklist ✅ readOnly = true por padrão, override explícito para escritas ✅ rollbackFor = Exception.class para incluir checked exceptions ✅ Timeout adequado conforme o tipo de operação ✅ Evitar chamadas internas (@Transactional ignorado) ✅ REQUIRES_NEW apenas quando commit independente é necessário ✅ MANDATORY para garantir contexto transacional ✅ Testes explícitos de comportamentos de rollback ✅ Monitorar transações de longa duração ✅ Documentar escolhas de propagação não padrão ``` ## Conclusão A propagação de transações é um conceito fundamental avaliado em entrevistas Spring Boot. Pontos-chave a lembrar: **Tipos de propagação comuns:** - ✅ REQUIRED (padrão): junta-se ou cria uma transação - ✅ REQUIRES_NEW: transação independente, commit separado - ✅ NESTED: savepoint para rollback parcial - ✅ MANDATORY: requer uma transação existente **Armadilhas a evitar:** - ✅ Self-invocation: ignora o proxy, @Transactional ignorado - ✅ Checked exceptions: sem rollback por padrão - ✅ REQUIRES_NEW nos mesmos dados: risco de deadlock - ✅ Sem timeout: transações bloqueadas indefinidamente **Melhores práticas:** - ✅ readOnly = true por padrão - ✅ rollbackFor = Exception.class sistematicamente - ✅ Serviços separados para evitar self-invocation - ✅ Testar explicitamente comportamentos de rollback Dominar a propagação de transações demonstra compreensão profunda do Spring e do gerenciamento de dados. Esses conceitos são essenciais para projetar aplicações robustas e passar com sucesso em entrevistas técnicas. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/spring-boot/spring-boot-transaction-propagation-interview