API REST para registrar livros que estou estudando, com seus capítulos e anotações. O foco do projeto não é o produto, e sim aplicar Domain-Driven Design de forma concreta em Spring: modelar Book como aggregate root e fazer a persistência refletir essa fronteira, em vez de tratar as três entidades como tabelas soltas.
É a reescrita em Java/Spring do book_queue, onde Chapter e Note eram entidades independentes, sem invariantes garantidas pelo domínio. Baseado no livro Domain Driven Design Quickly (versão digital gratuita).
O modelo tem três entidades numa hierarquia estrita — Book → Chapter → Note — e Book é a única acessível diretamente de fora. Isso está imposto em três camadas:
-
Persistência. Existe um único repositório:
BookRepository. Não háChapterRepositorynemNoteRepository. Capítulos e notas só são carregados e persistidos através doBook, comcascade = ALLeorphanRemoval = truecontrolando o ciclo de vida. A fronteira de persistência é a fronteira do agregado. -
Comportamento no domínio. As entidades são ricas. Criar um capítulo é
book.addChapter(...), que recusa título vazio ou duplicado no mesmo livro; adicionar uma nota échapter.addNote(...). Atualizações passam por métodos de domínio (updateDescription,updateSummary,updateContent) que validam antes de mutar. As regras vivem na entidade, não no service. -
Orquestração fina nos services. Os services carregam a raiz, delegam a operação ao domínio e salvam pela raiz — sem regra de negócio própria.
ChapterServiceeNoteServicedependem apenas deBookRepository, então não existe caminho para manipular um capítulo ou nota fora do seu livro.
A hierarquia aparece também nas rotas, sempre aninhadas:
/books/{bookId}/chapters/{chapterId}/notes/{noteId}
A aplicação emite e valida os próprios tokens JWT — não é um resource server que confia em um provedor externo. No POST /auth/login, o AuthenticationManager valida usuário e senha (BCrypt) e o JwtService assina um JWT RS256 com uma chave RSA privada e validade de 1 hora. A validação usa o oauth2-resource-server apenas como mecanismo: um JwtDecoder verifica a assinatura com a chave pública do mesmo par RSA. A sessão é stateless.
Autorização atual: GET é público, assim como /auth/login; qualquer outra requisição exige JWT válido.
Um @RestControllerAdvice traduz exceções em respostas HTTP padronizadas (timestamp, status, error, path), separando erros de domínio de infraestrutura:
| Exceção | HTTP | Origem |
|---|---|---|
ResourceNotFoundException |
404 | Raiz do agregado não encontrada |
EntityNotFoundException |
404 | Capítulo/nota inexistente no agregado |
DomainValidationException |
400 | Invariante de domínio violada |
BusinessRuleException |
409 | Regra de negócio (ex.: título duplicado) |
MethodArgumentNotValidException |
422 | Falha na validação dos DTOs, com erros por campo |
Os DTOs são validados com Bean Validation na entrada; o domínio revalida suas próprias invariantes de forma independente.
Java 17 · Spring Boot 3.5.11 (Web, Data JPA, Security) · Hibernate/JPA · PostgreSQL · OAuth2 Resource Server com assinatura RS256 (Nimbus) · Bean Validation · Lombok · springdoc/OpenAPI.