API REST para gerenciamento de portfólio de projetos. Projetos são criados e gerenciados localmente; membros são 100% externos, consultados e criados via API externa mockada (WireMock em testes).
- Gerencia o ciclo de vida completo de projetos (criação, atualização, transição de status, exclusão soft)
- Associa e desassocia membros funcionários a projetos
- Valida transições de status sequenciais com regras de negócio
- Calcula classificação de risco dinamicamente (orçamento + prazo)
- Delega operações de membros para API externa, com cache Redis e Circuit Breaker
- Gera relatório do portfólio com totais por status, orçamento e membros alocados
| Tecnologia | Uso |
|---|---|
| Java 21 | Linguagem |
| Spring Boot 3.5 | Framework principal |
| Spring Data JPA + Hibernate | Persistência |
| PostgreSQL | Banco de dados |
| Flyway | Migrations versionadas por domínio |
| Spring Security (Basic Auth) | Autenticação |
| Spring Cache + Redis | Cache distribuído |
| Resilience4j | Circuit Breaker na integração com API externa |
| Spring RestClient | HTTP client para API externa de membros |
| MapStruct | Mapeamento DTO ↔ Entidade |
| Lombok | Redução de boilerplate |
| SpringDoc OpenAPI | Documentação Swagger em /swagger-ui.html |
| Micrometer + Prometheus | Métricas em /actuator/prometheus |
| Testcontainers | PostgreSQL + Redis reais nos testes de integração |
| WireMock | Mock da API externa de membros nos testes |
| JaCoCo | Cobertura mínima de 70% em service e validation |
src/main/java/com/portfolio/api/
├── client/ # Integração com API externa de membros
│ ├── MembroExternalClient.java # Interface do client
│ └── MembroRestClientImpl.java # Implementação com CircuitBreaker
├── config/ # Configurações Spring
│ ├── CacheConfig.java # Redis + TTLs por cache
│ ├── JacksonConfig.java # ObjectMapper (BigDecimal, datas, Redis)
│ ├── JpaAuditConfig.java # Auditoria JPA (createdBy)
│ ├── OpenApiConfig.java # Swagger / OpenAPI
│ ├── RestClientConfig.java # Bean do RestClient para membros
│ └── SecurityConfig.java # Basic Auth, rotas públicas
├── controller/ # Endpoints REST
│ ├── MembroController.java # GET /membros, POST /membros
│ ├── ProjetoController.java # CRUD + status + membros
│ └── RelatorioController.java # GET /relatorios/portfolio
├── dto/
│ ├── request/ # Payloads de entrada
│ └── response/ # Payloads de saída
├── entity/ # Entidades JPA
│ ├── BaseEntity.java # id, createdAt, updatedAt, createdBy, active
│ ├── Projeto.java
│ └── ProjetoMembro.java # Tabela de associação N:N
├── enums/
│ ├── StatusProjeto.java # 8 status possíveis
│ └── ClassificacaoRisco.java # BAIXO, MEDIO, ALTO
├── exception/ # Exceções de domínio + handler global (RFC 7807)
├── filter/
│ └── CorrelationIdFilter.java # X-Correlation-ID para rastreio de logs
├── mapper/
│ └── ProjetoMapper.java # MapStruct: ProjetoCreateRequest → Projeto
├── repository/
│ ├── ProjetoRepository.java
│ └── ProjetoMembroRepository.java
├── service/ # Interfaces de serviço
│ └── impl/ # Implementações
├── specification/
│ └── ProjetoSpecification.java # Filtros dinâmicos JPA Criteria
└── validation/
├── RiskClassificationCalculator.java # Cálculo de risco por orçamento e prazo
└── StatusTransitionValidator.java # Máquina de estados do projeto
src/main/resources/
├── application.yml # Config base
├── application-dev.yml # Config local (datasource, redis)
├── application-prod.yml # Config produção
├── application-test.yml # Config testes (Testcontainers)
└── db/migration/
├── projeto/ V201__create_projeto_table.sql
└── projeto_membro/V301__create_projeto_membro_table.sql
| Método | Rota | Descrição |
|---|---|---|
| POST | /membros |
Cria membro na API externa |
| GET | /membros |
Lista todos os membros (API externa + cache) |
| GET | /membros/{externalId} |
Busca membro por ID externo |
| Método | Rota | Descrição |
|---|---|---|
| POST | /projetos |
Cria projeto (status inicial: EM_ANALISE) |
| GET | /projetos |
Lista paginada com filtros (nome, status, orçamento, datas) |
| GET | /projetos/{id} |
Busca projeto por ID |
| PUT | /projetos/{id} |
Atualiza projeto completo |
| PATCH | /projetos/{id} |
Atualiza projeto parcial |
| PATCH | /projetos/{id}/status |
Transição de status |
| DELETE | /projetos/{id} |
Soft delete |
| POST | /projetos/{id}/membros |
Associa membro ao projeto |
| DELETE | /projetos/{id}/membros/{externalId} |
Desassocia membro |
| Método | Rota | Descrição |
|---|---|---|
| GET | /relatorios/portfolio |
Relatório geral do portfólio |
EM_ANALISE
└─► ANALISE_REALIZADA
└─► ANALISE_APROVADA
└─► INICIADO ← requer ao menos 1 membro associado
└─► PLANEJADO
└─► EM_ANDAMENTO
└─► ENCERRADO (seta dataRealTermino automaticamente)
Qualquer status (exceto ENCERRADO e CANCELADO) → CANCELADO
| Risco | Orçamento | Prazo |
|---|---|---|
| BAIXO | ≤ R$100.000 | ≤ 3 meses |
| MEDIO | R$100.001 – R$500.000 | 3 a 6 meses |
| ALTO | > R$500.000 | > 6 meses |
Condições são OR — basta uma para elevar o risco. ALTO tem prioridade sobre MÉDIO.
- Apenas membros com
atribuicao = "gerente"podem ser definidos como gerente responsável (na criação e atualização do projeto) - Retorna HTTP 422 caso a atribuição seja diferente de
"gerente"
- Apenas membros com
atribuicao = "funcionário"podem ser associados a projetos - Máximo de 10 membros por projeto
- Máximo de 3 projetos ativos simultâneos por membro (excluídos ENCERRADO e CANCELADO)
- Desassociação mantém histórico (soft delete via
active = false) - Re-associação após desassociação é permitida
- Associação e desassociação são bloqueadas em projetos com status
ENCERRADOouCANCELADO(HTTP 422)
| Cache | TTL | Invalidação |
|---|---|---|
membros |
15 min | create no MembroService |
membros-lista |
15 min | create no MembroService |
projetos |
5 min | update, updateStatus, associarMembro, desassociarMembro, delete |
relatorio-portfolio |
5 min | updateStatus, delete |
Protege todas as chamadas à API externa de membros (create, findAll, findById).
| Parâmetro | Valor |
|---|---|
| Janela de avaliação | 10 chamadas |
| Threshold de falha | 50% |
| Threshold de chamadas lentas | 80% (acima de 3s) |
| Tempo em estado OPEN | 30s |
| Chamadas permitidas em HALF-OPEN | 3 |
| Transição automática OPEN → HALF-OPEN | habilitada |
Estados: CLOSED → OPEN (após falhas) → HALF-OPEN (tentativa) → CLOSED ou OPEN
O estado do circuit breaker é exposto em /actuator/health.
Basic Auth. Credenciais padrão (desenvolvimento):
- Usuário:
admin - Senha:
admin123
Rotas públicas (sem autenticação): /actuator/health, /actuator/info, /swagger-ui/**, /v3/api-docs/**
| Variável | Padrão | Descrição |
|---|---|---|
APP_EXTERNAL_MEMBRO_URL |
http://localhost:8081 |
URL da API externa de membros |
SPRING_DATASOURCE_URL |
via application-dev.yml |
URL do PostgreSQL |
SPRING_DATA_REDIS_HOST |
via application-dev.yml |
Host do Redis |
# Subir infraestrutura
docker compose up -d
# Rodar a aplicação
./mvnw spring-boot:run
# Rodar testes (unitários + integração)
./mvnw verifySwagger disponível em: http://localhost:8080/swagger-ui.html
A pasta json-server/ contém um servidor REST fake baseado em json-server, usado para simular a API externa de membros em desenvolvimento e testes manuais.
A arquitetura da API define que membros são um domínio totalmente externo — não há tabela local de membros. Em produção, existiria um serviço real. Para desenvolvimento local e testes manuais via Postman, o JSON Server oferece uma API REST funcional sem precisar subir outro serviço real, permitindo:
- Criar membros via
POST /membroscom UUID gerado automaticamente - Listar e buscar membros via
GET /membroseGET /membros/{id} - Persistir os dados em
db.jsonentre reinicializações - Rodar com um único comando Node, sem configuração de banco
Em testes automatizados (integração), o mock é feito via WireMock — mais controlado e sem dependência de processo externo.
cd json-server
npm install # apenas na primeira vez
node server.jsServidor disponível em: http://localhost:8081
| Método | Rota | Descrição |
|---|---|---|
| POST | /membros |
Cria membro (gera id UUID e createdAt automaticamente) |
| GET | /membros |
Lista todos os membros |
| GET | /membros/{id} |
Busca membro por ID |
{
"membros": [
{
"id": "035ea832-5aba-4a5c-b302-a874eeb6b7fb",
"nome": "adson",
"atribuicao": "funcionário",
"createdAt": "2026-04-01T20:54:53.534Z"
}
]
}O campo
idretornado pelo JSON Server é mapeado paraexternalIdnoMembroResponsevia@JsonAlias("id").
Toda requisição recebe um X-Correlation-ID no header de resposta. Se o cliente enviar o header na requisição, o mesmo valor é propagado. O ID aparece em todos os logs da thread via MDC — útil para correlacionar logs de uma requisição específica.