Backend production-ready projetado com foco em previsibilidade, observabilidade e isolamento de responsabilidades.
🔐 Autenticação JWT com Refresh Token Rotation
🧠 Arquitetura em camadas bem definida
🗄 PostgreSQL + Flyway (versionamento automático)
⚡ Cache distribuído com Redis
📊 Observabilidade completa (Micrometer + Prometheus + Grafana + Zipkin)
🧪 Testes de integração com Testcontainers (banco real)
🚀 CI/CD com quality gate obrigatório (80%+ cobertura)
☁️ Upload de imagens de capa com AWS S3
- Visão Geral
- Requisitos
- Quick Start
- Variáveis de Ambiente
- Problema que Resolve
- Stack Tecnológica
- Arquitetura
- Decisões Arquiteturais
- Observabilidade
- Estratégia de Testes
- Endpoints Principais
- Upload de Imagens (AWS S3)
- Agendamentos (Scheduled Jobs)
- Métricas do Projeto
- Próximos Passos
- Screenshots
- Contribuições
- Autor
- Docker 20.10+ & Docker Compose 2.0+
- Git 2.30+
- Java 25 (Eclipse Temurin recomendado)
- Gradle 9+ (ou use o wrapper
./gradlew)
docker --version # Docker version 20.10+
docker compose version # Docker Compose version 2.0+
git --version # git version 2.30+A Library API simula um backend de produção real para gerenciar livros, autores, categorias, usuários e empréstimos. Vai além de um CRUD — implementa segurança, cache distribuído, observabilidade, upload de arquivos e CI/CD completo.
O projeto possui dois modos de execução:
- dev → ambiente voltado para desenvolvimento e avaliação
- prod → ambiente containerizado simulando produção
git clone https://github.com/erichiroshi/library-api.git
cd library-apiNesse modo a infraestrutura é executada via Docker e a aplicação pode ser iniciada via container ou IDE.
docker compose -f docker-compose.dev.yml up -dA rede library-api_backend é criada automaticamente.
Serviços iniciados:
- PostgreSQL:
localhost:5432 - Redis:
localhost:6379 - pgAdmin: http://localhost:5050 (login
admin@admin.com/admin) - Prometheus: http://localhost:9090
- Grafana: http://localhost:3000 (login
admin/admin) - Zipkin: http://localhost:9411
Opção A — Container:
docker build -t library-api:latest .
docker run -d --network library-api_backend -p 8080:8080 --env-file .env.dev library-api:latestOpção B — IDE:
./gradlew clean buildRefresh Gradle project → Executar a aplicação
Acesse:
Usuário admin para teste:
Email: joao.silva@email.com
Senha: 123456
Características do profile dev:
- ✅ Swagger habilitado
- ✅ Banco populado com seed inicial (Flyway)
- ✅ Delay artificial de 2s no
GET /books/{id}para demonstrar cache Redis - ✅ Access token de 30 minutos (mais conveniente)
- ✅ Logs detalhados (DEBUG)
Executa toda a stack containerizada utilizando o profile prod.
docker compose up -dCaracterísticas do profile prod:
- ✅ Swagger desabilitado
- ✅ Banco de dados inicial vazio
- ✅ Configuração mais restritiva (HikariCP tunado)
- ✅ Ambiente totalmente containerizado
- ✅ Stateless (JWT) + cache compartilhado (Redis)
- ✅ Access token de 15 minutos
- ✅ Apenas endpoints
/actuator/healthe/actuator/prometheuspúblicos
Populando banco em prod:
docker exec -i library-api-postgres-1 psql -U postgres -d library < seed_realistic_dataset.sqldocker compose down # Para os containers
docker compose down -v # Para e remove volumes (apaga banco)Copie o arquivo de exemplo e preencha:
cp .env.example .env| Variável | Descrição | Exemplo |
|---|---|---|
SPRING_PROFILES_ACTIVE |
Profile ativo | prod ou dev |
DB_URL |
URL JDBC do PostgreSQL | jdbc:postgresql://postgres:5432/library |
DB_USERNAME |
Usuário do banco | postgres |
DB_PASSWORD |
Senha do banco | postgres |
JWT_SECRET_KEY |
Chave secreta JWT (mín. 256 bits) | — |
REDIS_HOST |
Host do Redis | redis |
REDIS_PORT |
Porta do Redis | 6379 |
AWS_KEY |
AWS Access Key ID | — |
AWS_SECRET |
AWS Secret Access Key | — |
BUCKET_NAME |
Nome do bucket S3 | library-api-s3 |
BUCKET_REGION |
Região do bucket | sa-east-1 |
⚠️ O arquivo.envestá no repositório apenas para fins educacionais. Em produção real, use um secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.).
Importe a collection para testar a API:
📁 Library-API.postman_collection.json (na raiz do projeto)
Este projeto vai além de um CRUD básico — ele simula desafios reais de produção:
Uma biblioteca precisa:
- ✅ Gerenciar empréstimos com regras (controle de cópias disponíveis)
- ✅ Autenticar usuários de forma segura (JWT + Refresh Token Rotation)
- ✅ Garantir performance em consultas frequentes (Cache Redis)
- ✅ Armazenar imagens de capa dos livros (AWS S3)
- ✅ Monitorar saúde e métricas da aplicação (Observabilidade)
- ✅ Garantir qualidade de código (80%+ cobertura obrigatória)
- ✅ Evoluir schema sem quebrar produção (Flyway migrations)
- ✅ Limpar dados expirados automaticamente (Scheduled Jobs)
- 🔐 Segurança: JWT com token rotation (previne replay attacks)
- ⚡ Performance: Cache distribuído com Redis + atomic decrement de cópias
- 📊 Observabilidade: Prometheus + Grafana + OpenTelemetry + Zipkin (tracing distribuído com traceId nos logs)
- ☁️ Storage: Upload de imagens com compressão automática via AWS S3
- 🧪 Qualidade: 80%+ cobertura com threshold obrigatório no CI
- 🚀 CI/CD: Quality gate automático (SonarCloud + Codecov)
- 🐳 DevOps: Docker Compose com 6 serviços orquestrados
- Java 25
- Spring Boot 4.x
- Spring Web MVC (API REST)
- Spring Data JPA (persistência)
- Spring Security (JWT)
- Spring Cache (Redis)
- Spring Actuator (health + métricas)
- Hibernate (ORM)
- Lombok (redução de boilerplate)
- PostgreSQL 16 (banco relacional)
- Flyway (versionamento de schema — 9 migrations)
- Schema per Service (schemas
auth,catalogelendingisolados no mesmo banco)
- Redis 7 (cache distribuído com TTL de 2 minutos)
- AWS S3 (upload de imagens de capa)
- Compressão e redimensionamento automático (máx. 400px de largura)
- Validação de content-type (PNG, JPEG, WEBP)
- Validação de tamanho (1KB mín. / 10MB máx.)
- Metadados automáticos no objeto S3
- Spring Actuator (health checks)
- Micrometer (abstração de métricas)
- Prometheus (coleta de métricas, scrape a cada 10s)
- Grafana (dashboards provisionados automaticamente)
- OpenTelemetry + Zipkin (tracing distribuído com traceId nos logs)
- Resilience4j (Circuit Breaker + Retry para integrações externas)
- ShedLock (lock distribuído para scheduled jobs)
- Testcontainers (PostgreSQL real em testes de integração)
- JUnit 5
- Mockito
- JaCoCo (cobertura com threshold de 80%)
- Docker & Docker Compose (6 serviços orquestrados)
- GitHub Actions (CI/CD — 4 workflows)
- Dependabot (atualização automática de dependências)
- Swagger/OpenAPI 3 (habilitado no profile
dev) - SonarCloud (quality gate)
- Codecov (tracking de cobertura)
- Jackson (JSON, com
non_nullpor padrão) - DTOs (isolamento de domínio)
- MapStruct (mapeamento automático)
- Bean Validation (validação declarativa)
┌─────────────────────────────────────────────┐
│ Controllers (REST Layer) │
│ @RestController / @RequestMapping │
│ • BookController │
│ • LoanController │
│ • AuthController │
│ • AuthorController │
│ • CategoryController │
└──────────────┬──────────────────────────────┘
│ DTOs (Request/Response)
┌──────────────▼──────────────────────────────┐
│ Services (Business Logic) │
│ @Service / @Transactional │
│ • BookService │
│ • LoanService │
│ • AuthService / RefreshTokenService │
│ • BookMediaService │
│ • LookupServices (anti-corruption layer) │
└──────────────┬──────────────────────────────┘
│ Entities
┌──────────────▼──────────────────────────────┐
│ Repositories (Data Access) │
│ JpaRepository │
│ • BookRepository │
│ • LoanRepository │
│ • UserRepository │
│ • RefreshTokenRepository │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ PostgreSQL + Redis + AWS S3 │
└─────────────────────────────────────────────┘
com.example.library/
├── auth/ # Autenticação (login, refresh, logout)
├── author/ # Gerenciamento de autores
├── aws/ # Integração AWS S3 + utilitários de imagem
├── book/ # Gerenciamento de livros (com cache)
├── category/ # Gerenciamento de categorias
├── common/ # BaseEntity, exceções, configurações comuns
├── config/ # CacheConfig, JpaConfig, SchedulingConfig
├── loan/ # Empréstimos e itens de empréstimo
├── refresh_token/ # Refresh tokens + cleanup job agendado
├── security/ # JWT filter, SecurityConfig, profiles dev/prod
├── swagger/ # Configuração OpenAPI
└── user/ # Entidade User + UserDetailsService
| Contexto | Responsabilidade | Schema |
|---|---|---|
auth |
Autenticação, usuários, refresh tokens | auth |
catalog |
Livros, autores, categorias | catalog |
lending |
Empréstimos e itens de empréstimo | lending |
Application → Actuator → Micrometer → Prometheus → Grafana
↓ ↓
OpenTelemetry Dashboards
↓
Zipkin (tracing distribuído)
Request → Controller → Service → [Cache Hit? → Return]
↓
Cache Miss
↓
Repository → PostgreSQL
↓
[Cache Store no Redis]
Caches configurados:
books— lista paginada (evict ao criar/deletar)bookById— busca por ID (evict ao deletar)- TTL global: 2 minutos
Por quê: Evitar race condition em empréstimos concorrentes.
Implementação: @Modifying com UPDATE ... WHERE availableCopies > 0 — o banco rejeita o UPDATE se não há cópias, sem necessidade de lock explícito. clearAutomatically = true invalida o cache de 1º nível do JPA após o UPDATE.
Por quê: Evita vazamento de regra de negócio para a camada HTTP.
Benefício: Regras podem ser reutilizadas por diferentes camadas (REST, scheduled jobs, listeners).
Por quê: Isolamento de domínio e controle explícito de exposição.
Benefício: Entidades JPA nunca expostas diretamente — previne lazy loading exceptions e vazamento de dados sensíveis.
Por quê: Independente da camada web.
Benefício: Cache funciona se chamado por REST, mensageria ou scheduled job.
Por quê: Segurança — não revelar que um empréstimo existe quando o usuário não tem permissão para acessá-lo.
Por quê: Demonstrar o efeito do cache Redis de forma perceptível.
Implementação: Interface ArtificialDelayService com duas implementações — DevArtificialDelayService (2s de sleep) e NoOpArtificialDelayService — selecionadas por @Profile.
Por quê: Banco real nos testes de integração.
Benefício: Testes simulam produção (PostgreSQL real), não comportamento idealizado do H2 in-memory.
Por quê: Pipeline falha abaixo de 80%.
Benefício: Garante qualidade mínima em cada PR, evitando degradação gradual.
Por quê: Preparação para extração em microservices.
Benefício: Código relacionado fica junto; cada pacote é praticamente auto-contido.
Por quê: Preparação para separação futura em microservices sem introduzir Kafka prematuramente.
Benefício: Domínios se comunicam via eventos internos (ApplicationEventPublisher) em vez de injeção direta de repositórios entre pacotes. Troca futura por Kafka/RabbitMQ requer mudança mínima.
Por quê: Proteger o monolito contra falhas de serviços externos (S3) e preparar os pontos de integração para extração futura.
Benefício: Circuit Breaker evita cascata de falhas; Retry com backoff trata falhas transitórias. Padrão já estabelecido para quando LoanService precisar chamar BookService via HTTP.
Por quê: BookService injetava AuthorRepository e CategoryRepository diretamente. LoanService injetava BookRepository e UserRepository. Injeção direta de repositórios entre domínios cria acoplamento estrutural que impede extração futura em microservices.
Implementação: Cada domínio expõe uma interface de lookup (AuthorLookupService, CategoryLookupService, BookLookupService, UserLookupService). Outros domínios dependem da interface, nunca do repositório.
Benefício: Trocar a implementação de uma chamada local para HTTP/Feign requer mudança apenas na implementação da interface, sem tocar nos serviços consumidores.
Por quê: O filtro anterior chamava userDetailsService.loadUserByUsername() em toda requisição autenticada, mesmo com as roles já presentes no JWT — gerando 1 query desnecessária por request.
Implementação: JwtAuthenticationFilter constrói o Authentication apenas com claims do token. JwtService.extractRoles() lê as roles diretamente do JWT, sem tocar no banco.
Benefício: Elimina 1 query ao banco por request autenticado.
Por quê: BookService misturava lógica de domínio com integração de infraestrutura (S3).
Implementação: BookMediaService encapsula todo o pipeline de upload: validação, redimensionamento e envio ao S3. BookService nunca depende de S3 diretamente.
Benefício: BookService testável sem mock de S3; infraestrutura substituível sem tocar em regras de negócio.
Por quê: Microservices exigem database per service — cada serviço deve possuir e controlar suas próprias tabelas. Separar bancos imediatamente seria prematuro; separar schemas é o passo intermediário seguro.
Implementação: Três schemas criados via Flyway (V008/V009): auth, catalog e lending. Entidades anotadas com @Table(schema = "..."), tabelas de junção com @JoinTable(schema = "...") e coleções com @CollectionTable(schema = "..."). HikariCP configurado com search_path para resolução automática.
Benefício: Fronteiras de dados explícitas sem complexidade operacional de múltiplos bancos. Migração futura para bancos separados requer apenas apontar cada serviço para seu próprio PostgreSQL.
Métricas expostas:
- JVM (memória, threads, GC)
- HTTP (requests, latência, status codes)
- Database (pool de conexões)
- Cache Redis (hits, misses, evictions)
- Custom de negócio (ver abaixo)
Métricas customizadas de negócio:
library.books.created— Counter de livros criados
Alertas configurados no Prometheus (alerts.yml):
HighErrorRate— taxa de erros 5xx acima de 0.05/s por 5 minutos (warning)HighMemoryUsage— uso de heap JVM acima de 90% por 5 minutos (critical)
Dashboards Grafana (provisionados automaticamente):
- Total de livros
- Requests por segundo (RPS)
- Requests por endpoint
- Erros 5xx por segundo
- Tempo médio de resposta (ms)
- Taxa de erro (%)
Cada request recebe um traceId único propagado por toda a chamada e registrado nos logs.
Request → Application → OpenTelemetry SDK → Zipkin
↓
traceId injetado nos logs (SLF4J MDC)
↓
Zipkin UI: visualização de spans e latência
Propagação W3C Trace Context — padrão adotado, compatível com qualquer ferramenta de APM.
Acesso:
- Prometheus: http://localhost:9090
- Grafana: http://localhost:3000 (admin/admin)
- Zipkin: http://localhost:9411
- Métricas raw: http://localhost:8080/actuator/prometheus
- Health: http://localhost:8080/actuator/health
Pirâmide de Testes:
/\
/ \ E2E (poucos)
/____\
/ \ Integration (médio)
/ \
/__________\ Unit (muitos)
- Isolamento de regra de negócio
- Mockito para dependências
- Foco em Services
@DataJpaTest(context slice)- Banco H2 in-memory (rápido)
- Valida queries customizadas (
findOverdueLoans,countActiveByUserId,decrementCopies)
@SpringBootTest(context completo)- Testcontainers com PostgreSQL real
- Profile
it— cache desabilitado (@Profile("!it")noCacheConfig) - Valida fluxo end-to-end
Cobertura atual: 80%+
Threshold obrigatório: 80% (pipeline falha se menor)
Exclusões de cobertura: DTOs, configs, mappers gerados
Executar testes:
./gradlew test # Unit + Repository tests
./gradlew integrationTest # Integration tests
./gradlew test integrationTest # Todos os testes
./gradlew jacocoTestReport # Gerar relatório de coberturaRelatório HTML: build/reports/jacoco/test/html/index.html
| Método | Endpoint | Descrição | Auth |
|---|---|---|---|
| POST | /auth/login |
Login — retorna access + refresh token | ❌ |
| POST | /auth/refresh |
Renova access token (token rotation) | ❌ |
| POST | /auth/logout |
Invalida o refresh token | ❌ |
| Método | Endpoint | Descrição | Auth |
|---|---|---|---|
| GET | /api/v1/books |
Lista livros paginado (com cache Redis) | ✅ |
| GET | /api/v1/books/{id} |
Busca por ID (com cache Redis) | ✅ |
| POST | /api/v1/books |
Cria livro | ✅ |
| DELETE | /api/v1/books/{id} |
Remove livro | 🔐 ADMIN |
| POST | /api/v1/books/{id}/cover |
Upload de imagem de capa (S3) | ✅ |
| Método | Endpoint | Descrição | Auth |
|---|---|---|---|
| GET | /api/v1/authors |
Lista autores paginado | ✅ |
| GET | /api/v1/authors/{id} |
Busca por ID | ✅ |
| POST | /api/v1/authors |
Cria autor | ✅ |
| DELETE | /api/v1/authors/{id} |
Remove autor | 🔐 ADMIN |
| Método | Endpoint | Descrição | Auth |
|---|---|---|---|
| GET | /api/v1/categories |
Lista categorias paginado | ✅ |
| GET | /api/v1/categories/{id} |
Busca por ID | ✅ |
| POST | /api/v1/categories |
Cria categoria | 🔐 ADMIN |
| DELETE | /api/v1/categories/{id} |
Remove categoria | 🔐 ADMIN |
| Método | Endpoint | Descrição | Auth |
|---|---|---|---|
| POST | /api/v1/loans |
Cria empréstimo | ✅ |
| GET | /api/v1/loans/{id} |
Busca por ID (apenas dono ou ADMIN) | ✅ |
| GET | /api/v1/loans/me |
Lista meus empréstimos | ✅ |
| GET | /api/v1/loans |
Lista todos os empréstimos | 🔐 ADMIN |
| GET | /api/v1/loans/user/{userId} |
Lista empréstimos por usuário | 🔐 ADMIN |
| GET | /api/v1/loans/overdue |
Lista empréstimos vencidos | 🔐 ADMIN |
| PATCH | /api/v1/loans/{id}/return |
Registra devolução | ✅ |
| PATCH | /api/v1/loans/{id}/cancel |
Cancela empréstimo | ✅ |
Documentação interativa (profile dev): http://localhost:8080/swagger-ui/index.html
POST /api/v1/books/{id}/cover
Content-Type: multipart/form-data
O pipeline de upload:
- Validação de tamanho (1KB mín. / 10MB máx.)
- Validação de content-type (
image/png,image/jpeg,image/webp) - Redimensionamento automático para máx. 400px de largura (mantém aspect ratio)
- Upload para S3 com metadados (
uploaded-by,original-filename,upload-timestamp) - URL pública salva em
tb_book.cover_image_url - URL retornada no header
Location
Para usar o S3, você precisa de credenciais AWS com permissão de s3:PutObject e s3:GetObject no bucket configurado.
# No .env ou variáveis de ambiente:
AWS_KEY=sua-access-key
AWS_SECRET=seu-secret
BUCKET_NAME=seu-bucket
BUCKET_REGION=sa-east-1💡 Para desenvolvimento local sem AWS, você pode usar LocalStack como alternativa.
Limpa automaticamente refresh tokens expirados do banco de dados.
- Frequência: Todo dia às 02:00 AM (
cron = "0 0 2 * * *") - O que faz:
DELETE FROM tb_refresh_tokens WHERE expiry_date < NOW() - Lock distribuído: ShedLock garante execução em apenas uma instância (
lockAtLeastFor = "30m",lockAtMostFor = "1h") - Por quê: Tokens expirados são deletados ao serem usados (via
validate()), mas tokens nunca reutilizados acumulam no banco.
Marca como OVERDUE empréstimos com status = WAITING_RETURN e dueDate < hoje.
💡 O método
markOverdue()está implementado noLoanServicee pode ser exposto via@Scheduledou endpoint admin conforme necessidade.
- ~8.000 linhas de código
- 125+ testes (unit + integration)
- 80%+ cobertura (JaCoCo)
- 30+ endpoints REST versionados (
/api/v1) - 6 serviços Docker orquestrados
- 9 migrations Flyway
- 4 workflows GitHub Actions (CI, Docker, Release, README PDF)
- 3 bounded contexts isolados por schema (
auth,catalog,lending)
- Rate limiting — Resilience4j
- OpenTelemetry + Zipkin — Tracing distribuído com W3C Trace Context
- Bounded contexts — schemas isolados, anticorrupção e LookupServices
- Revisão pré-Fase 3 — JWT filter otimizado, BookMediaService extraído, workflows corrigidos
- Fase 3 — Extração em Microservices (branch
microservices)- Config Repo + Config Server
- Eureka Server (service discovery)
- Spring Cloud Gateway (JWT centralizado)
- Auth Service
- Catalog Service
- Loan Service
- Deploy em cloud — AWS ECS ou Render
- HATEOAS — Hypermedia links
- WebSockets — Notificações real-time de devolução
- LocalStack — Suporte a S3 local em testes de integração
📋 Acompanhe o progresso da Fase 3 no GitHub Projects
Contribuições são muito bem-vindas!
- Fork o repositório
git clone https://github.com/SEU-USER/library-api.git- Crie uma branch de feature
git checkout -b feature/nova-funcionalidade-
Faça suas mudanças
- Adicione testes (cobertura mínima 80%)
- Rode
./gradlew test integrationTest - Verifique qualidade:
./gradlew sonar
-
Commit seguindo Conventional Commits
git commit -m "feat: adiciona endpoint de busca avançada"- Push e abra um Pull Request
git push origin feature/nova-funcionalidadePRs são revisados em até 48h com feedback construtivo garantido.
Eric Hiroshi
Backend Engineer — Java / Spring Boot
- 💼 LinkedIn: Eric Hiroshi
- 📧 Email: erichiroshi@hotmail.com
- 🔗 GitHub: @erichiroshi
Este projeto está sob a licença MIT.
A versão em PDF é gerada automaticamente via GitHub Actions e está disponível na aba Releases e como artefato nos workflows.
"Código limpo é aquele que expressa a intenção com simplicidade e precisão."
Desenvolvido com ☕ e 💻
Se este projeto te ajudou de alguma forma, considere dar uma ⭐ no repositório!



