Plataforma fullstack de backoffice para operações de investimento
Status: MVP funcionalmente completo — todas as fases do roadmap entregues (autenticação fica fora do MVP por decisão, ADR 0006). O SPEC.md descreve a visão completa; as evoluções mapeadas estão nos ADRs.
⚠️ Demo local — não exponha na internet. A stack sobe com credenciais de demonstração e publica Postgres/RabbitMQ no host para facilitar o onboarding; não há autenticação por decisão de escopo (ADR 0006). Use apenas na máquina local — qualquer deploy real exige sobrescrever segredos, fechar as portas e colocar auth na frente.
- Arquitetura event-driven com Outbox Pattern nos dois lados — API .NET e Worker Go publicam via relay transacional (sem dual-write) por RabbitMQ.
- Resiliência no consumo: retry com backoff (wait queue + TTL) e Dead Letter Queue; falhas transitórias reprocessam, falhas de negócio são terminais.
- Interop poliglota: serviço de cotações em gRPC (Go) consumido pela API .NET, com Protobuf versionado.
- Modular monolith .NET 8 (Clean Architecture, Dapper, DbUp) + SPA Angular 21 zoneless (signals, standalone,
OnPush, sem zone.js) servida por edge nginx same-origin. - Qualidade: testes com Testcontainers, CI no GitHub Actions, config por ambiente, rate limiting por perfil de endpoint, 9 ADRs (índice) e specs de design por fase em docs/.
Prints da aplicação — todos os dados são fictícios (seed local).
Dashboard — métricas do pipeline, funil de status e auto-refresh.
| Operações | Conciliação |
|---|---|
![]() |
![]() |
| Import com validação, idempotência e status por operação. | Extrato do custodiante: divergências, resolver/ignorar. |
| Processing Monitor | Produtos |
|---|---|
![]() |
![]() |
| Eventos do worker com tentativas/erro reais e auto-refresh. | CRUD com ticker único e exclusão lógica (linha inativa esmaecida). |
O SPEC.md é a visão completa; a entrega é incremental, por fases (docs/design/):
| Área | Status |
|---|---|
| Import de operações (validação, idempotência, Outbox → RabbitMQ → Worker) (ADR 0003) | ✅ |
| Worker Go: regras de negócio, posição consolidada, retry + DLQ (ADR 0004) | ✅ |
| Read API: operations, positions, dashboard, lookups (paginação/ordenação server-side) | ✅ |
| Frontend: dashboard, operações, posições, cotações ao vivo | ✅ |
| Cotações via gRPC (.NET ↔ Go) + SSE | ✅ |
| Edge nginx (same-origin) + rate limiting | ✅ |
| Conciliação com custodiante: import CSV, divergências, resolver/ignorar (ADR 0007) | ✅ |
CRUD de produtos (POST/PUT/DELETE lógico, ticker único) |
✅ |
| Processing monitor: eventos do worker + falha terminal com tentativas/erro (ADR 0008) | ✅ |
| Autenticação/autorização (OIDC) | 🚫 fora do MVP — ADR 0006 |
flowchart LR
SPA["Angular 21 SPA<br/>zoneless"]
EDGE["edge nginx<br/>same-origin"]
API[".NET 8 API<br/>modular monolith"]
WK["Go Worker<br/>retry + DLQ"]
MD["Go MarketData<br/>gRPC"]
MQ{{RabbitMQ}}
PG[("PostgreSQL")]
SPA <-->|"/ e /api"| EDGE
EDGE --> API
API -->|"operação + outbox<br/>(mesma transação)"| PG
API -.->|"Outbox relay · source=api"| MQ
MQ -->|OperationImported| WK
WK -->|"posições + processing_events"| PG
WK -.->|"Outbox relay · source=worker"| MQ
API <-->|gRPC| MD
Quatro serviços — SPA Angular, API .NET, Worker Go e MarketData Go — sobre banco compartilhado no MVP (ADR 0005). Integração assíncrona via Outbox Pattern nos dois lados (ADR 0003) + RabbitMQ; cotações via gRPC e stream ao vivo por SSE. No deploy, um edge nginx serve o SPA e faz proxy de /api para a API (same-origin). Todas as decisões — com limitações e evolução — em docs/adr/.
| Camada | Tecnologias |
|---|---|
| Frontend | Angular 21 (zoneless, signals), TypeScript, RxJS, Angular Material, Jest |
| API | .NET 8, ASP.NET Core, Dapper, FluentValidation, Serilog, OpenTelemetry |
| Worker | Go, RabbitMQ, pgx, slog |
| MarketData | Go, gRPC, Protobuf (buf) |
| Infra | Docker Compose, PostgreSQL, RabbitMQ, nginx |
investops-backoffice/
backend/ # solução .NET 8
src/
InvestOps.Api/ # ASP.NET Core (Serilog, Swagger, Health Checks)
InvestOps.Application/ # casos de uso
InvestOps.Domain/ # entidades e enums do domínio
InvestOps.Infrastructure/ # persistência/Dapper, RabbitMQ, cliente gRPC
InvestOps.Contracts/ # DTOs de evento
InvestOps.Migrator/ # DbUp: schema + seed
tests/ # UnitTests, IntegrationTests
worker-go/ # worker assíncrono (consumo RabbitMQ, retry + DLQ)
marketdata-go/ # serviço gRPC de cotações
frontend/investops-web/ # app Angular 21 (zoneless)
contracts/ # contratos de eventos JSON + .proto (gRPC)
docs/ # ADRs e documentação de design
docker-compose.yml # stack completo
Há dois caminhos:
- A. Stack completo via Docker — um comando sobe todos os serviços. Ideal para ver o sistema fim a fim.
- B. Componente por componente — infra no Docker, cada serviço no host, com hot-reload. Ideal para desenvolver.
- Docker + Docker Compose — obrigatório em ambos os caminhos
- .NET 8 SDK — API, Migrator e testes .NET
- Go 1.26+ — worker e marketdata
- Node.js 20+ — frontend Angular
Builda e sobe todos os serviços: postgres, rabbitmq, migrator (schema + seed), marketdata, worker, api e web.
cp .env.example .env # opcional; o compose já usa defaults
docker compose --env-file .env.example up -d --build
docker compose --env-file .env.example ps # aguarde postgres/rabbitmq (healthy)- App (SPA + API via edge nginx): http://localhost:8080
- RabbitMQ Management UI: http://localhost:15672 (
investops/investops)
Parar tudo:
docker compose --env-file .env.example down # mantém os dados (volumes)
docker compose --env-file .env.example down -v # apaga os dadosSuba apenas a infra pelo Docker e rode cada serviço no host. Os defaults de cada serviço já apontam para localhost, então nenhuma variável de ambiente é obrigatória em dev. Siga a ordem abaixo (a API depende de MarketData; o Worker e a API dependem de Postgres + RabbitMQ).
docker compose --env-file .env.example up -d postgres rabbitmqSem os nomes dos serviços,
docker compose upsubiria tudo (inclusive api/worker/web já buildados).
cd backend
dotnet run --project src/InvestOps.Migrator -- --seed # schema + dados de exemplo
# ou apenas o schema:
dotnet run --project src/InvestOps.Migratorcd marketdata-go
go run ./cmd/server # escuta gRPC em :5300 (env GRPC_PORT)cd worker-go
go run ./cmd/worker # consome RabbitMQ, aplica regras e faz o relay do OutboxRetry/backoff/DLQ e demais variáveis estão em worker-go/README.md.
cd backend
dotnet run --project src/InvestOps.Api- Swagger: http://localhost:5000/swagger (a raiz
/redireciona para o Swagger em Development/Staging) - Health: http://localhost:5000/health · liveness
/health/live· readiness/health/ready - Consome o MarketData em
http://localhost:5300(configMarketData:Url)
Em
Productiono Swagger é desabilitado; apenas/healthfica exposto.
cd frontend/investops-web
npm install
npm start # ng serve em http://localhost:4200Em dev o SPA chama a API direto em http://localhost:5000/api (src/environments/environment.ts).
A configuração é separada por ambiente via ASPNETCORE_ENVIRONMENT, carregando appsettings.{Environment}.json sobre o appsettings.json base.
| Ambiente | Arquivo | Serilog | Swagger |
|---|---|---|---|
| Development | appsettings.Development.json |
Debug | ✅ |
| Staging (homologação) | appsettings.Staging.json |
Information | ✅ |
| Production | appsettings.Production.json |
Warning | ❌ |
Rodar em um ambiente específico (perfis em launchSettings.json):
dotnet run --project src/InvestOps.Api --launch-profile staging
dotnet run --project src/InvestOps.Api --launch-profile production
# ou direto:
ASPNETCORE_ENVIRONMENT=Staging dotnet run --project src/InvestOps.ApiSegredos (Staging/Production) não ficam nos arquivos — vêm de variáveis de ambiente, que sobrescrevem o appsettings (__ separa seções):
export ConnectionStrings__Postgres="Host=...;Database=...;Username=...;Password=..."
export RabbitMq__Host="rabbit.prod.interno"
export RabbitMq__Username="..."
export RabbitMq__Password="..."
export MarketData__Url="http://marketdata.prod.interno:5300"Decisões de segurança do MVP — registradas em ADR 0006:
- Rate limiting nativo (ASP.NET Core): limite global de 100 req/min + política mais rígida (20 req/min) nos endpoints de escrita (
import,reprocess); health checks isentos;429comRetry-After. Limites configuráveis via seçãoRateLimiting. - Autenticação/autorização: fora do escopo do MVP, por decisão — backoffice interno assume rede privada + IdP corporativo; o ADR documenta como o design acomoda
[Authorize]+ policies por perfil. - SQL 100% parametrizado (Dapper) com ordenação dinâmica por whitelist; Swagger desabilitado em Production (com teste garantindo); security headers no nginx.
- Credenciais: o
appsettings.jsonbase não contém credenciais — as de dev local vivem emappsettings.Development.json, e Staging/Production só via variáveis de ambiente. Os defaultsinvestops/investopsdo.env.example/compose são intencionais e exclusivos de demo local (docker compose upsem edição é requisito); qualquer deploy real deve sobrescrevê-los.
# Backend (.NET) — unit + integração (testcontainers exige Docker)
cd backend && dotnet test
# Worker e MarketData (Go) — integração usa testcontainers (exige Docker)
cd worker-go && go test ./...
cd marketdata-go && go test ./...
# Frontend (Angular) — Jest
cd frontend/investops-web && npm test| Serviço | Dev (host) | Docker |
|---|---|---|
App — edge nginx (SPA + /api) |
— | 8080 |
| .NET API | 5000 | interna (via edge) |
| Angular dev server | 4200 | — |
| MarketData (gRPC) | 5300 | 5300 |
| PostgreSQL | 5432 | 5432 |
| RabbitMQ (AMQP) | 5672 | 5672 |
| RabbitMQ Management UI | 15672 | 15672 |
Decisões conscientes de MVP, com o caminho de evolução registrado nos ADRs:
- Autenticação/autorização (OIDC) — fora do MVP por decisão; o design acomoda
[Authorize]+ policies por perfil (ADR 0006). - Banco compartilhado → database-per-service — um PostgreSQL entre API e worker hoje; a fronteira por módulo já isola o caminho de separação (ADR 0005).
- Dead-letter do Outbox — ambos os publishers reconectam após queda; falta cap + dead-letter de mensagem "poison" — assimetria consciente com o consumer, que já tem DLQ (ADR 0009).
- Conciliação síncrona → assíncrona — comparação no request hoje; extratos grandes pedem processamento em background (ADR 0007).
- Rate limiting por IP → por identidade — hoje particiona por IP; com auth, migra para
X-Forwarded-For/subject (ADR 0006).
main— estável (release por fase)developer— integraçãofeature/*— uma branch por task, PR paradeveloper
Commits seguem Conventional Commits.




