Microsserviço de Catálogo de Produtos desenvolvido com Clean Architecture, DDD, Hexagonal Architecture, SOLID e o padrão arquitetural CQRS-Lite (Command Query Responsibility Segregation).
✨ Foco: Separação estrita de responsabilidades, inversão de dependência e domínio desacoplado da infraestrutura.
Feito com muito café ☕ + conhecimento humano 🧠 e auxílio de IAs para ideias, pesquisas e refinamentos rápidos.
O ms-product-catalog é um microsserviço de catálogo de produtos que implementa:
- Arquitetura Hexagonal (Ports & Adapters): isolamento total entre o núcleo de negócio e os detalhes de infraestrutura.
- Domain-Driven Design (DDD): Aggregate Root, Value Objects, Domain Services e Notification Pattern.
- CQRS com Domain Bypass: separação física de adaptadores de leitura e escrita, com o lado de leitura ignorando completamente as entidades de domínio ricas (bypass direto para DTOs).
- CQS no nível de código: todos os métodos de comando retornam
voidou apenas identificadores; nunca retornam dados de leitura. - GraalVM Native Image: compilação AOT para startup ultrarrápido (~50–200ms) e footprint de memória mínimo.
Fonte da Imagem: Domain-Driven Hexagon
| Categoria | Tecnologia | Versão mínima | Obs.: |
|---|---|---|---|
| Linguagem | Java | 21 | |
| Framework | Spring Boot | 4.0.3 | |
| Banco de Dados | PostgreSQL | 18.3 | |
| Compilação Nativa | GraalVM Native Image | 21.0.2 | |
| Resiliência | Resilience4j (Circuit Breaker) | 2025.1.0 (via Spring Cloud) | |
| Documentação API | SpringDoc OpenAPI 3 (Swagger UI) | 3.0.1 | |
| Análise de Qualidade | SonarQube Cloud | — | |
| Testes Unitários | JUnit + AssertJ + Mockito | via Spring Boot + 3.27.7 | Para regras de negócio (Domain) e fluxos (UseCases). |
| Testes de Integração | Testcontainers (PostgreSQL) | via Spring Boot | Para repositórios JPA (Persistence), garantindo que rodem no PostgreSQL via Testcontainers. |
| Testes de API Externa | Contract Stub Runner (WireMock) | 2025.1.0 (via Spring Cloud) | |
| Testes E2E | REST Assured | 6.0.0 | Para validar as chamadas na API Interna (Controllers) |
| Testes de Arquitetura | ArchUnit | 1.4.1 | |
| Cobertura de Código | JaCoCo | via Gradle | |
| Build | Gradle (Kotlin DSL) | 9.3.1 |
- JDK 21 instalado.
- Docker e Docker Compose ou Podman instalados no ambiente local.
1. Clone o repositório:
git clone https://github.com/wallanpsantos/ms-product-catalog.git
cd ms-product-catalog2. Configure as variáveis de ambiente:
Crie um arquivo .env na raiz do projeto (já listado no .gitignore):
cp .env.example .env # ou crie manualmenteDB_USERNAME=admin
DB_PASSWORD=change-me-in-production
DB_NAME=db-product-catalog
PGADMIN_USER=admin@admin.com
PGADMIN_PASSWORD=change-me-in-production3. Suba a infraestrutura (PostgreSQL + pgAdmin):
Com Docker:
docker compose up -dCom Podman:
podman compose up -d4. Compile e execute a aplicação:
./gradlew bootRunOu compile o JAR e execute:
./gradlew clean build -x nativeCompile
java -jar build/libs/ms-product-catalog-1.0.0.jarPara compilar o executável nativo com startup ultrarrápido:
./gradlew nativeCompile
./build/native/nativeCompile/ms-product-catalogNota: A compilação nativa pode levar alguns minutos. Em CI, o passo
nativeCompileé ignorado com-x nativeCompilepara agilizar o pipeline.
Execute todos os testes (unitários + API (Interna e Externa) + integração via Testcontainers):
./gradlew testO relatório de cobertura JaCoCo é gerado automaticamente em build/reports/jacoco/test/.
Execute apenas testes de arquitetura:
./gradlew test --tests "*ArchTest*"Execute apenas testes de integração:
./gradlew test --tests "*IntegrationTest*"Após iniciar a aplicação, acesse:
| Interface | URL |
|---|---|
| Swagger UI | http://localhost:8080/swagger-ui.html |
| OpenAPI JSON | http://localhost:8080/v3/api-docs |
| Health Check | http://localhost:8080/actuator/health |
| pgAdmin | http://localhost:8081 |
Um script completo com exemplos de chamadas para todos os endpoints está disponível em
docs/curl-examples.sh.
curl -X POST http://localhost:8080/api/v1/products \
-H "Content-Type: application/json" \
-d '{
"name": "Smartphone Galaxy S24",
"description": "256GB, Titanium Gray",
"category": "Eletronicos",
"brand": "Samsung",
"price": 5999.00,
"active": true
}'curl http://localhost:8080/api/v1/products/{id}curl "http://localhost:8080/api/v1/products?page=0&perPage=10&sort=name&dir=asc"curl -X POST http://localhost:8080/api/v1/products/search \
-H "Content-Type: application/json" \
-d '{ "query": "samsung" }'curl -X POST http://localhost:8080/api/v1/products/batch \
-H "Content-Type: application/json" \
-d '[
{ "name": "Produto A", "description": "Desc A", "category": "Cat", "brand": "Brand", "price": 100.0, "active": true },
{ "name": "Produto B", "description": "Desc B", "category": "Cat", "brand": "Brand", "price": 200.0, "active": true }
]'curl -X DELETE http://localhost:8080/api/v1/products/{id}
# HTTP 204 No Contentflowchart TB
Client(["🌐 Client (HTTP)"])
Client --> Controller
subgraph Infrastructure ["⚙️ Infrastructure"]
Controller["ProductController\n(Driving Adapter)"]
subgraph Adapters ["JPA Adapters"]
direction TB
CMD["ProductJpaCommandAdapter\nimplements ProductCommandGateway"]
QRY["ProductJpaQueryAdapter\nimplements ProductQueryGateway"]
end
end
subgraph Application ["📦 Application Layer"]
direction LR
subgraph Commands ["Commands (Write)"]
C1["CreateProduct"]
C2["UpdateProduct"]
C3["DeactivateProduct"]
end
subgraph Queries ["Queries (Read)"]
Q1["GetProductById"]
Q2["ListActiveProducts"]
Q3["SearchProducts"]
end
end
subgraph Domain ["🔷 Domain (Zero Dependencies)"]
AGG["Product\n(Aggregate Root)"]
VAL["ProductValidator"]
AGG --> VAL
end
DB[("🐘 PostgreSQL")]
Controller --> Commands
Controller --> Queries
Commands --> CMD
Queries --> QRY
CMD -->|" load / persist\nAggregate "| AGG
CMD <-->|" INSERT / UPDATE "| DB
QRY -->|" Domain Bypass\nProduct.java ignorado "| DB
DB -->|" ProductJpaEntity\n→ ProductSummary (DTO) "| QRY
style Domain fill: #1a3a5c, color: #fff, stroke: #4a9ede
style Application fill: #1a4a2e, color: #fff, stroke: #4aaa6e
style Infrastructure fill: #3a2a1a, color: #fff, stroke: #cc8844
style Commands fill: #0d3320, color: #fff, stroke: #4aaa6e
style Queries fill: #0d3320, color: #fff, stroke: #4aaa6e
style Adapters fill: #2a1a0a, color: #fff, stroke: #cc8844
style QRY fill: #1a3a1a, color: #90ee90, stroke: #4aaa6e
style CMD fill: #3a1a1a, color: #ffaaaa, stroke: #cc4444
sequenceDiagram
participant API as Controller
participant UC as GetProductById (Query)
participant GW as ProductJpaQueryAdapter
participant DB as PostgreSQL
Note over UC, DB: DOMAIN BYPASS — Product.java nunca é instanciado
API ->> UC: execute(Input)
UC ->> GW: findSummaryById(id)
GW ->> DB: findById(id)
DB -->> GW: ProductJpaEntity
GW ->> GW: toSummary(Entity)
GW -->> UC: ProductSummary (DTO)
UC -->> API: Output (DTO)
sequenceDiagram
participant API as Controller
participant UC as UpdateProduct (Command)
participant GW as ProductJpaCommandAdapter
participant Dom as Product (Aggregate)
participant DB as PostgreSQL
API ->> UC: execute(Input)
UC ->> GW: findById(id)
GW ->> DB: findById(id)
DB -->> GW: Entity
GW -->> UC: Product (Entity)
UC ->> Dom: update(dados)
Dom ->> Dom: validate()
UC ->> GW: update(Product)
GW ->> DB: save(Document)
UC -->> API: Output(id apenas)
POST /api/v1/products — Criar Produto
sequenceDiagram
autonumber
participant C as Client (HTTP)
participant CTRL as ProductController
participant UC as DefaultCreateProductUseCase
participant DOM as Product (Domain)
participant VAL as ProductValidator
participant GW as ProductJpaCommandAdapter
participant DB as PostgreSQL
C ->> CTRL: POST /products (JSON)
CTRL ->> UC: execute(Input)
UC ->> DOM: newProduct(...)
UC ->> DOM: validate(Notification)
DOM ->> VAL: validate()
alt Notification has Errors
UC -->> CTRL: Throw NotificationException
CTRL -->> C: 422 Unprocessable Entity
else Success
UC ->> GW: create(Product)
GW ->> DB: save(Entity)
DB -->> GW: Saved Entity
GW -->> UC: Product (Persisted)
UC -->> CTRL: Output(ID)
CTRL -->> C: 201 Created + Location Header
end
POST /api/v1/products/batch — Criar em Lote
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as BatchUseCase
participant DOM as Product
participant GW as Gateway
participant DB as PostgreSQL
C ->> CTRL: POST /batch (List<Request>)
CTRL ->> UC: execute(List<Input>)
loop For each Item
UC ->> DOM: newProduct(...)
UC ->> DOM: validate()
opt Error
UC -->> CTRL: Throw NotificationException
end
end
UC ->> GW: createAll(List<Product>)
GW ->> DB: saveAll(List<Entity>)
DB -->> GW: Saved Entities
GW -->> UC: List<Product>
UC -->> CTRL: List<Output>
CTRL -->> C: 201 Created (List<Response>)
GET /api/v1/products/{id} — Busca por ID
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as GetByIdUseCase
participant GW as QueryAdapter
participant DB as PostgreSQL
C ->> CTRL: GET /products/{id}
CTRL ->> UC: execute(id)
UC ->> GW: findSummaryById(id)
GW ->> DB: findOne(id)
alt Found
DB -->> GW: Entity
GW -->> UC: ProductSummary
UC -->> CTRL: Output
CTRL -->> C: 200 OK
else Not Found
DB -->> GW: null
UC -->> CTRL: Throw NotFoundException
CTRL -->> C: 404 Not Found
end
GET /api/v1/products — Listagem Paginada
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as ListUseCase
participant GW as QueryAdapter
participant DB as PostgreSQL
C ->> CTRL: GET /?page=0&perPage=10
CTRL ->> UC: execute(Pageable)
UC ->> GW: findAllActiveSummary(Pageable)
GW ->> DB: find({active: true}).skip(0).limit(10)
DB -->> GW: List<Entity>
GW -->> UC: Page<ProductSummary>
UC -->> CTRL: Page<Output>
CTRL -->> C: 200 OK (Page JSON)
PUT /api/v1/products/{id} — Atualizar Produto
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as UpdateUseCase
participant DOM as Product
participant GW as CommandAdapter
participant DB as PostgreSQL
C ->> CTRL: PUT /products/{id}
CTRL ->> UC: execute(Input)
UC ->> GW: findById(id)
alt Not Found
GW -->> UC: empty
UC -->> CTRL: Throw NotFoundException
CTRL -->> C: 404 Not Found
else Found
GW -->> UC: Product (Existing)
UC ->> DOM: update(fields...)
UC ->> DOM: validate(Notification)
UC ->> GW: update(Product)
GW ->> DB: save(Entity)
UC -->> CTRL: Output(id)
CTRL -->> C: 200 OK
end
PUT /api/v1/products/batch — Atualizar em Lote (Bulk Read Anti N+1)
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as BatchUpdateUC
participant GW as CommandAdapter
participant DB as PostgreSQL
C ->> CTRL: PUT /batch (List<UpdateReq>)
CTRL ->> UC: execute(List<Input>)
Note right of UC: 1. Busca em Massa (evita N+1)
UC ->> GW: findAllById(ids)
GW ->> DB: find({_id: {$in: ids}})
DB -->> GW: List<Entities>
Note right of UC: 2. Processamento em Memória
alt Missing IDs
UC -->> CTRL: Throw NotFoundException
else All Found
UC ->> UC: Update & Validate (Loop)
UC ->> GW: updateAll(List<Product>)
GW ->> DB: saveAll(Entities)
GW -->> UC: List<Product>
UC -->> CTRL: List<Output>
CTRL -->> C: 200 OK
end
DELETE /api/v1/products/{id} — Desativar (Soft Delete)
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as DeactivateUC
participant DOM as Product
participant GW as CommandAdapter
participant DB as PostgreSQL
C ->> CTRL: DELETE /products/{id}
CTRL ->> UC: execute(id)
UC ->> GW: findById(id)
alt Found
GW -->> UC: Product
UC ->> DOM: deactivate()
UC ->> GW: update(Product)
GW ->> DB: save(Entity)
UC -->> CTRL: void
CTRL -->> C: 204 No Content
else Not Found
UC -->> CTRL: Throw NotFoundException
CTRL -->> C: 404 Not Found
end
POST /api/v1/products/search — Pesquisa Textual
sequenceDiagram
autonumber
participant C as Client
participant CTRL as Controller
participant UC as SearchUseCase
participant GW as QueryAdapter
participant DB as PostgreSQL
C ->> CTRL: POST /search (query="samsung")
CTRL ->> UC: execute(Input)
UC ->> GW: searchProductsSummary("samsung")
Note right of GW: Like %<br/>Campos: name, description, category, brand
GW ->> DB: find({ $or: [...], active: true })
DB -->> GW: List<Entity>
GW -->> UC: List<ProductSummary>
UC -->> CTRL: List<Output>
CTRL -->> C: 200 OK (Results)
- SonarQube Cloud: análise contínua de vulnerabilidades, bugs e code smells em cada push.
- Dependabot: monitoramento automático do grafo de dependências via GitHub Dependency Graph.
- Gradle Wrapper Validation: o checksum do
gradle-wrapper.jaré validado contra o registro oficial do Gradle antes de qualquer compilação no CI, prevenindo ataques à cadeia de suprimento. - Imagem Docker não-root: o container executa com usuário
appuser(sem privilégios de root). - Secrets via variáveis de ambiente: credenciais nunca são hardcoded; gerenciadas por
.env(gitignored) ou um secrets manager em produção. no-new-privileges: opção Docker que impede que processos internos ao container adquiram privilégios adicionais pós-startup.
| Documento | Descrição |
|---|---|
| Estrutura do Projeto | Detalhamento de cada camada e decisões arquiteturais |
| Padrões CQS & CQRS | Guia de arquitetura com exemplos e diagramas |
| Modelagem de Dados | Diagrama ER e Dicionário de Dados do banco relacional |
| curl-examples.sh | Scripts prontos para testar todos os endpoints |
Veja LICENSE para mais informações.
