Arquitetura Java — Spring Boot
Estrutura de Pacotes (Monolito Modular)
com.company.{app}/
├── {boundedcontext}/ # ex: order, payment, catalog
│ ├── domain/
│ │ ├── model/ # Entidades, Aggregates, Value Objects
│ │ ├── event/ # Domain Events
│ │ ├── repository/ # Interfaces (ports)
│ │ └── exception/ # DomainException tipadas
│ ├── application/
│ │ ├── service/ # Use Cases (@Service, 1 classe = 1 caso de uso)
│ │ ├── dto/ # Command/Query DTOs
│ │ └── port/ # Ports de infra (EmailPort, PaymentPort)
│ ├── infrastructure/
│ │ ├── persistence/ # JPA entities, Repository impl
│ │ ├── messaging/ # Kafka producers/consumers
│ │ └── config/ # @Configuration do módulo
│ └── interfaces/
│ ├── rest/ # @RestController
│ └── listener/ # @KafkaListener
└── shared/ # kernel compartilhado (mínimo!)
├── exception/
└── util/
Regra: módulos não importam infrastructure de outros bounded contexts — apenas application ports ou eventos.
Clean Architecture com Spring
// domain/model/Order.java — SEM anotações Spring/JPA no núcleo puro (opção 1)
public class Order {
private final OrderId id;
private final CustomerId customerId;
private OrderStatus status;
private final List<OrderItem> items = new ArrayList<>();
public void addItem(OrderItem item) {
if (status != OrderStatus.PENDING) {
throw new OrderNotEditableException(id);
}
items.add(item);
}
public void confirm() {
if (items.isEmpty()) throw new EmptyOrderException(id);
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(id, customerId));
}
}
// application/service/CreateOrderService.java
@Service
@RequiredArgsConstructor
public class CreateOrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher events;
@Transactional
public OrderResponse execute(CreateOrderCommand cmd) {
var order = Order.create(cmd.customerId(), cmd.items());
orderRepository.save(order);
order.pullDomainEvents().forEach(events::publishEvent);
return OrderResponse.from(order);
}
}
// interfaces/rest/OrderController.java — FINO
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderService createOrderService;
@PostMapping
public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest req) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(createOrderService.execute(req.toCommand()));
}
}
DDD — Padrões Essenciais
Value Object (Record)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Amount cannot be negative");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new CurrencyMismatchException();
return new Money(amount.add(other.amount), currency);
}
}
Aggregate + Domain Events
public class Order {
@Transient
private final List<DomainEvent> domainEvents = new ArrayList<>();
protected void registerEvent(DomainEvent event) { domainEvents.add(event); }
public List<DomainEvent> pullDomainEvents() {
var events = List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
}
Repository (Port)
// domain/repository/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
Order save(Order order);
List<Order> findPendingByCustomer(CustomerId customerId);
}
// infrastructure/persistence/JpaOrderRepository.java
@Repository
@RequiredArgsConstructor
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpa;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId id) {
return jpa.findById(id.value()).map(mapper::toDomain);
}
}
Event-Driven
- Domain Event → publicado in-process via
ApplicationEventPublisher
- Integration Event → Kafka após Outbox (ver
~/.cursor/skills/references/event-driven-patterns.md)
- Consumidores idempotentes (
@KafkaListener + dedup por event_id)
Monolito Modular vs Microsserviços
| Critério |
Monolito Modular |
Microsserviço |
| Time |
< 15 eng, 1 produto |
Times autônomos por domínio |
| Deploy |
Único artefato |
Deploy independente |
| Consistência |
Transação local ACID |
Saga / eventual consistency |
| Quando |
Default — comece aqui |
Bounded context maduro + SLA distinto |
Anti-Patterns
| Anti-Pattern |
Problema |
Solução |
@Autowired field injection |
Teste difícil, acoplamento |
Constructor injection + @RequiredArgsConstructor |
@Service God Object |
Viola SRP |
1 use case por classe |
Optional em entidade |
Semântica confusa |
Nullable explícito ou Value Object |
Lógica no @RestController |
Fat controller |
Delegar para application service |
| Anemic Domain Model |
Regras fora do domínio |
Comportamento nas entidades |
@Transactional no controller |
Transação errada |
Mover para application layer |
Referências
~/.cursor/skills/references/springboot-structure.md
~/.cursor/skills/references/ddd-patterns.md
~/.cursor/skills/references/event-driven-patterns.md