⤴️ Índice- 📖 Sobre
- 💻 Rodando o Projeto
- 🌐 Ambiente
- 🐋 Containerizado
- 🏠 Local
- 📰 Documentação da API
- ✅ Testes Unitários & Integração
- 🐋 Containerizado
- 🏠 Local
- ⚙️ Executando Testes
- 🚚 Testes Carga & Performance
- 🕵️ Observabilidade WIP
- 🧑🔧 Validação Manual
- 📊 Diagramas
- 🅻 Questão Aberta L4
- 🧠 ADR - Architecture Decision Records
- 🔢 Versões
- 🧰 Ferramentas
- 👏 Boas Práticas
- 🤖 Uso de IA
- 🏁 Conclusão
Resumo:
Desafio de
Payment Authorizer
de benefícios emHexagonal Architecture
com100ms SLA
por request e controle de concorrência com baixa possibilidade de colisão. Construído comGin
eGorm
, protocologRPC
entre o serviçoREST
HTTP
("aberto" ao mundo, o ponto de entrada) e o serviçoProcessor
("fechado" ao mundo, o processador de pagamentos) por segurança.Principais Tecnologias e abordagens:
Hexagonal Architecture
TDD
,DDD
,SOLID
,ADRs
REST
HTTP
egRPC
entreMicrosservices
Dockerized
Solução com uso de containersPostgreSQL
modelado inspirado emEvent Sourcing
garantindoConsistency
Concurrent Programming
Redis
paraPessimistic Memory Lock
Redis Keyspace Notification
comoPub/Sub
para notificarUnlocks
(Robust Queues
foram desconsideradas devidoAdditional Latency
)CI
comGitHub Actions
Diagram as code
comMermaid.js
eMiro
Performance/Load Test Dockerized
comGatling
Observability
(WIP)
Prometheus
eGrafana
paraRED Metrics
Grafana Loki
ePromtail
paraLogs
Após concluir os requisitos obrigatórios (L1
, L2
, L3
) no prazo, retomei o desafio, focando melhorias do diagrama Miro
criado em conjunto a equipe proponente. Implementei o requisito L4
como aprimoramento técnico sugerido no diagrama e na ADR
0003: gRPC e Redis Keyspace Notification reduzindo Latência e evitando Concorrência.
Texto Original:
Acompanhe as tarefas pelo Kanban
Este repositório foi criado com a intenção de propor uma possível solução para o seguinte desafio:
👨💻 Desafio Técnico:
Além de avaliar a correção da sua solução, temos interesse em ver como você modela o domínio, organiza seu código e implementa seus testes.
Linguagem e bibliotecas:
Na *********, usamos Scala e Kotlin no nosso dia a dia (e demonstrar experiência em alguma delas é um grande diferencial). No entanto, você pode implementar sua solução utilizando sua linguagem favorita, dando preferência ao paradigma de programação funcional.
Como entregar a solução?
Entregue a sua solução preferencialmente criando um repositório git (Github, Gitlab, etc).
É muito importante escrever um arquivo README com as instruções para execução do projeto.
Agora, vamos guiá-lo através de alguns conceitos básicos.
Transaction
Uma versão simplificada de um transaction payload de cartão de crédito é o seguinte:
{ "account": "123", "totalAmount": 100.00, "mcc": "5811", "merchant": "PADARIA DO ZE SAO PAULO BR" }Atributos
id - Um identificador único para esta transação.
accountId - Um identificador para a conta.
amount - O valor a ser debitado de um saldo.
merchant - O nome do estabelecimento.
mcc - Um código numérico de 4 dígitos que classifica os estabelecimentos comerciais de acordo com o tipo de produto vendido ou serviço prestado.
O
MCC
contém a classificação do estabelecimento. Baseado no seu valor, deve-se decidir qual o saldo será utilizado (na totalidade do valor da transação). Por simplicidade, vamos usar a seguinte regra:
- Se o
mcc
for"5411"
ou"5412"
, deve-se utilizar o saldo deFOOD
.- Se o
mcc
for"5811"
ou"5812"
, deve-se utilizar o saldo deMEAL
.- Para quaisquer outros valores do
mcc
, deve-se utilizar o saldo deCASH
.
Desafios (o que você deve fazer)
Cada um dos desafios a seguir são etapas na criação de um autorizador completo. Seu autorizador deve ser um servidor HTTP que processe a transaction payload JSON usando as regras a seguir.
As possíveis respostas são:
{ "code": "00" }
se a transação é aprovada{ "code": "51" }
se a transação é rejeitada, porque não tem saldo suficiente{ "code": "07" }
se acontecer qualquer outro problema que impeça a transação de ser processadaO HTTP Status Code é sempre
200
L1. Autorizador simples
- O autorizador simples deve funcionar da seguinte forma:
- Recebe a transação
- Usa apenas a MCC para mapear a transação para uma categoria de benefícios
- Aprova ou rejeita a transação
- Caso a transação seja aprovada, o saldo da categoria mapeada deverá ser diminuído em totalAmount.
L2. Autorizador com fallback
- Para despesas não relacionadas a benefícios, criamos outra categoria, chamada CASH. O autorizador com fallback deve funcionar como o autorizador simples, com a seguinte diferença:
- Se a MCC não puder ser mapeado para uma categoria de benefícios ou se o saldo da categoria fornecida não for suficiente para pagar a transação inteira, verifica o saldo de CASH e, se for suficiente, debita esse saldo.
L3.Dependente do comerciante
- As vezes, os MCCs estão incorretos e uma transação deve ser processada levando em consideração também os dados do comerciante. Crie um mecanismo para substituir MCCs com base no nome do comerciante. O nome do comerciante tem maior precedência sobre as MCCs.
- Exemplos:
UBER TRIP SAO PAULO BR
UBER EATS SAO PAULO BR
PAG*JoseDaSilva RIO DE JANEI BR
PICPAY*BILHETEUNICO GOIANIA BR
L4. Questão aberta
- A seguir está uma questão aberta sobre um recurso importante de um autorizador completo (que você não precisa implementar, apenas discuta da maneira que achar adequada, como texto, diagramas, etc.).
- Transações simultâneas: dado que o mesmo cartão de crédito pode ser utilizado em diferentes serviços online, existe uma pequena mas existente probabilidade de ocorrerem duas transações ao mesmo tempo. O que você faria para garantir que apenas uma transação por conta fosse processada em um determinado momento? Esteja ciente do fato de que todas as solicitações de transação são síncronas e devem ser processadas rapidamente (menos de 100 ms), ou a transação atingirá o timeout.
Para este teste, tente ao máximo implementar um sistema de autorização de transações considerando todos os desafios apresentados (L1 a L4) e conceitos básicos.
O desafio sugere Scala
, Kotlin
e o paradigma funcional
, mas aceita outras linguagens e paradigmas. Realizei em Golang
, com arquitetura Hexagonal
, por maior experiência e familiaridade, além de ser mencionada na job description
como parte do stack utilizado. Essa combinação atende bem aos requisitos do desafio.
Contudo, sou aberto a expandir minhas habilidades e disposto a aprender novas tecnologias e paradigmas conforme necessário.
Docker
e Docker Compose
são necessários para rodar a aplicação de forma containerizada, e é fortemente recomendado utilizá-los para rodar o banco de dados localmente.
Crie uma copia do arquivo ./payments-api/.env.SAMPLE
e renomeie para ./payments-api/.env
.
Recomendado
Após a .env
renomeada, rode os comandos docker compose
(de acordo com sua versão do docker compose) no diretório raiz do projeto
# Construir a imagem
docker compose build
# Rodar o PostgreSQL e o Redis de Desenvolvimento
docker compose up postgres redis -d
Caso seja a primeira execução do projeto, rode os comandos de migrations
e seeds
para dar carga inicial no banco de dados de desenvolvimento.
# Rodar Migrations de Desenvolvimento
docker compose up migrate
# Carga inicial no banco
docker compose exec postgres psql -U api_user -d payments_db -f /seeds/load_test_charge.up.sql
Para então subir os serviços REST
e Processor
# Rodar as APIs (Sugiro em terminais distintos para acompanhar debug logs)
docker compose up transaction-processor
docker compose up transaction-rest
A API está pronta e a rota da Documentação da API (Swagger) estará disponível, assim como os Testes poderão ser executados.
Apenas se necessário
Com o Golang 1.23
instalado e após ter renomeado a copia de .env.SAMPLE
para .env
, serão necessárias outras alterações para que a aplicação funcione corretamente no seu localhost
.
No arquivo .env
, substitua os valores das variáveis de ambiente que contêm comentários no formato local: valueA | containerized: valueB
pelos valores sugeridos na opção local
(exceto o DATABASE_PORT
).
DATABASE_HOST=localhost ### local: localhost | conteinerized: test-postgres
PUBSUB_HOST=localhost ### local: localhost | conteinerized: redis
IN_MEMORY_LOCK_HOST=localhost ### local: localhost | conteinerized: redis
IN_MEMORY_CACHE_HOST=localhost ### local: localhost | conteinerized: redis
GRPC_SERVER_HOST=localhost ### local: localhost | conteinerized: transaction-processor
GRPC_CLIENT_HOST=localhost ### local: localhost | conteinerized: transaction-processor
Após editar o arquivo, suba apenas o banco e o redis de dados com o comando:
# Rodar o PostgreSQL e o Redis de Desenvolvimento
docker compose up postgres redis -d
Caso seja a primeira execução do projeto, rode os comandos de migrations
e seeds
para dar carga inicial no banco de dados de desenvolvimento.
# Rodar Migrations de Desenvolvimento
docker compose up migrate
# Carga inicial no banco
docker compose exec postgres psql -U api_user -d payments_db -f /seeds/load_test_charge.up.sql
ou se conecte a database/redis válidos no arquivo .env
, então no diretório payments-api
execute os comandos:
# Instala Dependências
go mod download
# Rodar as APIs (Sugiro em terminais distintos para acompanhar debug logs)
go run cmd/processor/main.go
go run cmd/rest/main.go
A API está pronta e a rota da Documentação da API (Swagger) estará disponível, assim como os Testes poderão ser executados.
Com a aplicação em execução, a rota de documentação Swagger fica disponível em http://localhost:8080/swagger/index.html
A interface do Swagger pode executar Validação Manual a partir de requests
no endpoint POST: /payment
Recomendado
Para Executar os Testes usando container, é necessário que já esteja Rodando o Projeto Containerizado.
As configurações para executar os testes de repositório e integração (dependentes de infraestrutura) de maneira containerizada estão no arquivo ./payments-api/.env.TEST
. Não é necessário alterá-lo ou renomeá-lo, pois a API o usará automaticamente se a variável de ambiente ENV
estiver definida como test
.
Apenas se necessário
Para Executar os Testes com a API fora do container, de maneira local, é necessário editar seu /.env.TEST
.
No arquivo /.env.TEST
, substitua os valores das variáveis de ambiente que contêm comentários no formato local: valueA | containerized: valueB
pelos valores sugeridos na opção local
.
DATABASE_HOST=localhost ### local: localhost | conteinerized: test-postgres
DATABASE_PORT=5433 ### local: 5433 | conteinerized: 5432
PUBSUB_HOST=localhost ### local: localhost | conteinerized: redis
IN_MEMORY_LOCK_HOST=localhost ### local: localhost | conteinerized: redis
IN_MEMORY_CACHE_HOST=localhost ### local: localhost | conteinerized: redis
GRPC_SERVER_HOST=localhost ### local: localhost | conteinerized: transaction-processor
GRPC_CLIENT_HOST=localhost ### local: localhost | conteinerized: transaction-processor
Rodando o Projeto payment-api
em seu ambiente local ou containerizado, levante o banco de testes com
# Rodar o PostgreSQL de Testes
docker compose up test-postgres -d
Comando para executar o teste containerizado com a API levantada (Recomendado)
# Executa Testes no Docker com ENV test (PostgreSQL de Testes na Integração)
docker compose exec -e ENV=test transaction-rest go test -v -count=1 ./internal/adapter/repository/gormRepos ./internal/adapter/repository/redisRepos ./internal/core/service ./internal/adapter/http/router
Comando para executar o teste local em payments-api
(Apenas se necessário)
# Executa Testes Localmente com ENV test (PostgreSQL de Testes na Integração)
ENV=test go test -v -count=1 ./internal/adapter/repository/gormRepos ./internal/adapter/repository/redisRepos ./internal/core/service ./internal/adapter/http/router
Cada vez que o comando for executado, as tabelas e índices da base de dados testada serão truncados e recriados no banco de dados do ambiente selecionado garantindo uma execução segura e limpa.
Os testes também são executados como parte da rotina minima de CI
do GitHub Actions, garantindo que versões estáveis sejam mescladas na branch principal. O badge CI
no cabeçalho do arquivo readme é uma ajuda visual para verificar rapidamente a integridade do desenvolvimento.
Saída esperada do workload na fase test do github
Apenas Containerizado.
Atualmente o Gatling
na versão 3.9.5 (desatualizada), realiza os testes de carga. Para executá-los, é necessário estar Rodando o Projeto Containerizado. Em outro terminal, no diretório raiz do projeto, execute os seguintes comandos
# Rodar o Gatling
docker compose up gatling -d
# Executa o teste de carga
docker exec -ti gatling /entrypoint run-test
Saída esperada no client Redis de cache-inmemory (db0) e lock-inmemory (db1)
Saída esperada no site Gatling em seu localhost
O teste executa 7500k transações em 5 minutos (ou 25 TPS
, Transações Por Segundo
. Valor pico médio de tráfego de esperado pelo proponente do desafio, validado por benchmarks reais). O objetivo é validar o timeoutSLA
de 100ms
em uma máquina local. Essa configuração pode ser encontrada nas seguintes linhas do arquivo PaymentSimulation.scala:
private val tps = 25
private val window = 5.minutes
private val activeUsers = (window.toSeconds * tps).toInt
- Picos de
TPS
sugeridos:- 50, 100 : Cenários de carga regular em ambientes controlados (máquina local).
- Acima de 100: Considerados como
stress test
quando realizados em máquina local, devido à concorrência por recursos. - 200, 250, ...: Cenários de carga avançada para validação de
homologação/produção
. - Em ambientes de
homologação/produção
, espera-se que o sistema suporte valores superiores a250 TPS
(a validar). - Em ambientes de
homologação/produção
, a carga ofensora deve ser distribuída entre diferentespods
.
O comando abaixo remove o histórico dos testes de carga.
# Limpa os dados do teste de carga
docker exec -ti gatling /entrypoint clean-test
- Considerações
- Os resultados variam conforme os processos e a configuração da máquina. Recomenda-se executar com poucas aplicações rodando e monitorar via
htop
. - Para fins de comparação, os testes mais antigos permanecem no diretório
tests/gatling/results/history/
. - Os resultados são apenas referência para o desenvolvimento local
- Quando possível, valide em ambientes próximos à produção, eles tendem a ter perfomances superiores a máquina de desenvolvimento local.
- Os resultados variam conforme os processos e a configuração da máquina. Recomenda-se executar com poucas aplicações rodando e monitorar via
- Métricas Relevantes
Timeout
: tempo médio, mínimo e máximo de cada request.Erros
: Use logs de debug para mapear serviços e identificar gargalos. (Utilize as ferramentas de Observabilidade)
- Pré-produção e Stress Tests
- Nos ambientes de
pre-prod
estg
, se possível, use amostras maiores de dados próximos aos reais (TPS
,usuários médios
epicos históricos
). Realize tambémstress tests
, comprimindo cargas (e.g., simular 30 minutos de tráfego em 10). Esses testes ajudam a identificar falhas e garantem a escalabilidade progressiva.
- Nos ambientes de
Apenas Containerizado. Validado no SO Ubunto 22
Altere em seu arquivo .env
as configurações de métricas do database
para true
e o log_output
para loki
.
DATABASE_METRICS_ENABLED=true
...
LOG_OPT_OUTPUT=loki ### text | json | loki
Agora, Rodando o Projeto payment-api
em seu ambiente containerizado com seu .env
configurado, suba as imagens necessárias com o comando abaixo e reinicie o transaction-processor
e o transaction-rest
.
# Rodar o pushgateway, redis-exporter, prometheus e grafana
docker compose up pushgateway redis-exporter prometheus loki promtail grafana -d
Saída esperada no site Prometheus em seu localhost
Rodando a consulta gin_gonic_request_duration_bucket{path="/payment"}
Agora, acesse e configure o Grafana em seu localhost (usr/pwd: admin/admin)
A primeira vez que executarmos o Grafana, entramos com usr/pwd
padrão de admin/admin
. Ele solicita a alteração da senha, para facilitar o desenvolvimento local, alteramos para admin/12345
.
Criando conexão Datasource com Prometheus
No menu lateral procure por `Connections > Add New Connection` digite Prometheus no campo de Search, selecione-o, clique em `Add New Datasource` e configure-o com a URL: http://prometheus:9090 e clique no botão `Save & test` no final da página

Criando conexão Datasource com Loki
Volte para `Connections > Add New Connection` e dessa vez digite Loki no campo de Search, selecione-o, clique em `Add New Datasource` e configure-o com a URL: http://loki:3100 e clique no botão `Save & test` no final da página

Importando o Dashboard RED / USE (wip)
Agora você pode usar o menu `Dashboards > New > Import` para importar o arquivo dash-payments-api.json que está localizado no diretório: ./scripts/grafana-dashboards. Acesse o diretório em seu computador, clique e arraste o arquivo para o campo correto especificado pela tela Upload Dashboard JSON File, selecione o Prometheus previamente configurado como data source e proceda o import

Importando o Dashboard REDIS
Repita os passos de Importando o Dashboard RED / USE (wip) de dashboard anteriores para o arquivo dash-redis.json atentando-se para o fato de que o `Redis` não consome dados do `Loki`

*Imagem retirada durante teste de carga. **Dashboard baixado da GrafanaLabs
Consultando Logs no Grafana Loki
Agora é possível utilizar o `Grafana` para filtrar seus logs de forma mais eficiente. Acesse o menu `Home > Connections > Data sources`, localize a lista de `sources` e, na linha correspondente ao `Loki`, clique no botão `Explore`. Isso abrirá a seção de consultas de logs, permitindo uma análise detalhada dos dados.
Essa funcionalidade é especialmente poderosa quando utilizada em conjunto com os Testes de Carga & Performance, proporcionando insights valiosos para otimização e resolução de problemas ainda em desenvolvimento como visto abaixo em um exemplo de Erro 07 por timeOut.
Quando adequadamente importados, os Dashboards
REDIS
e RED / USE (wip)
estarão disponíveis e responderão às solicitações que você pode simular pela Documentação da API ou pelos Testes Carga & Performance (fortemente recomendado rodar em conjunto com a Observabilidade
) bem como filtors de logs
através do Grafana Loki
.
O banco de desenvolvimento local, quando adequadamente instalado, possui uma carga inicial de dados que pode ser utilizada para Validação Manual.
Registros e Saldos no banco para teste manual
accounts
Account: AcountID: 123e4567-e89b-12d3-a456-426614174000 1
Query Consulta Balances (saldo mais recente de transactions
por categories
, não é a mesma da repository
) por account:
SELECT
a.id as account_id,
a.uid as account_uid,
t.id as transaction_id,
t.uid as transaction_uid,
t.amount as amount,
c.id as category_id,
c.name as category_name,
c.priority as priority,
STRING_AGG(mc.mcc, ',') AS codes
FROM
accounts as a
JOIN
account_categories as ac ON ac.account_id = a.id
JOIN
categories as c ON c.id = ac.category_id
JOIN
transactions as t ON t.account_id = a.id
AND t.category_id = c.id
AND t.id = (
SELECT MAX(t2.id)
FROM transactions t2
WHERE t2.account_id = a.id AND t2.category_id = c.id
)
LEFT JOIN
mccs as mc ON mc.category_id = c.id
WHERE
a.uid = '123e4567-e89b-12d3-a456-426614174000'
GROUP BY a.id, a.uid, t.id, t.uid, t.amount, c.id, c.name, c.priority;
L1. L2. Resultado esperado:
account_id account_uid transaction_id transaction_uid amount category_id category_name priority codes 1 123e4567-e89b-12d3-a456-426614174000 1 [NULL] 1205.11 1 FOOD 1 5411,5412 1 123e4567-e89b-12d3-a456-426614174000 2 [NULL] 1110.22 2 MEAL 2 5811,5812 1 123e4567-e89b-12d3-a456-426614174000 3 [NULL] 1115.33 3 MOBILITY 3 6411 1 123e4567-e89b-12d3-a456-426614174000 4 [NULL] 1215.33 5 CASH 5
*Com acesso ao banco a partir dos dados de .env
, para validar. Bem como a Documentação da API (Swagger) pode ser utilizado para proceder as requests
.
L3. merchants
com mapeamentos MCC incorretos
Merchant MCCs Mapeado para Categoria UBER EATS SAO PAULO BR 5412 FOOD PAG*JoseDaSilva RIO DE JANEI BR 5812 MEAL ...
*Utilize o campo name
real da tabela merchant
, o github pode formatar de maneira incorreta esse dado no markdown.
*Diagramas Mermaid podem apresentar problemas de visualização em aplicativos mobile
erDiagram
accounts_categories {
int id PK
int account_id FK
int category_id FK
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
merchants {
int id PK
UUID uid
string name
int mcc_id FK
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
transactions {
int id PK
UUID uid
int account_id FK
int category_id FK
numeric amount
string mcc
string merchant_name
numeric total_amount
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
mccs {
int id PK
string mcc
int category_id FK
datetime created_at
datetime updated_at
timestamp deleted_at
}
categories {
int id PK
UUID uid
string name
int priority
datetime created_at
datetime updated_at
timestamp deleted_at
}
accounts {
int id PK
UUID uid
string name
datetime created_at
datetime updated_at
timestamp deleted_at
}
transactions_latest {
int account_id PK
int category_id PK
int transactions_latest_id
numeric amount
}
categories ||--o{ transactions : has
categories ||--o{ mccs : has
categories ||--o{ accounts_categories : defines
mccs ||--o{ merchants : has
accounts_categories }o--|| accounts : has
transactions }o--|| accounts : has
accounts Tabela principal, conectada a transactions e accounts_categories, armazenando informações sobre as contas.
accounts_categories Vincula contas a categorias associadas.
categories Armazena categorias (FOOD, MEAL, CASH...), com priority
para definir a ordem de utilização.
mccs Contém MCCs (códigos de quatro dígitos) associados às categorias.
merchants Ajusta MCCs com base no nome do comerciante.
transactions Registra o histórico de transações realizadas, incluindo categoria, comerciante e valores.
transactions_latest: Tabela auxiliar para reduzir o tempo de consulta às transações recentes das contas. Atualizada através da trigger trg_update_latest_transaction
.
Autorização de Pagamento
flowchart TD
A([▶️<br/>Recebe Transação JSON]) --> B[Mapeia Categoria pelo nome do comerciante]
B --> C[Buscar Saldos da Conta]
C --> D{Saldo é suficiente <br/> na Categoria?}
D -- Sim --> E[Debita Saldo da Categoria]
D -- Não --> F{Saldo suficiente na <br/> Categoria e CASH?}
F -- Sim --> G[Debita Categoria e CASH]
F -- Não --> H{Saldo suficiente em CASH?}
H -- Sim --> I[Debita Saldo de CASH]
H -- Não --> J[Rejeita Transação com Código 51]
E --> K[Registrar Transação Aprovada]
G --> K[Registrar Transação Aprovada]
I --> K[Registrar Transação Aprovada]
K --> P{Ocorreu Erro no Processo da Transação?}
P -- Sim --> Q[❌<br/><b>Rejeitada</b><br/> Retorna Código <b>07</b> por Falha Genérica</b>]
P -- Não --> M[✅<br/><b>Aprovada</b><br/> Retorna Código <b>00</b>]
J --> N[❌<br/><b>Rejeitada</b><br/> Retorna Código <b>51</b> por Saldo Insuficiente</b>]
N --> O([⏹️<br/>Fim do Processo])
M --> O
Q --> O
style M fill:#009933,stroke:#000
style N fill:#cc0000,stroke:#000
style Q fill:#cc0000,stroke:#000
*Diagrama apresenta uma interpretação do sistema
-
Recebe Transação JSON: O sistema recebe o payload de transação.
-
Mapeia MCC pelo Merchant Name: Busca um relacionamento entre o
merchant
e uma categoria adequada. Caso categoria Não exista segue o fluxo para debitar de CASH -
Buscar Saldos da Conta: A conta e os saldos (FOOD, MEAL, CASH) são buscados no banco de dados
-
Saldo é suficiente na Categoria?: Verifica se o saldo disponível na categoria mapeada (com base no MCC) é suficiente.
- Se sim, debita o saldo da categoria correspondente.
- Se não, verifica o saldo de CASH.
-
Saldo suficiente em CASH?: Se a categoria principal não tiver saldo suficiente, o sistema verifica o saldo de CASH.
- Se sim, debita parcial ou totalmente o saldo de CASH.
- Se não, rejeita a transação com o código "51" (fundos insuficientes).
-
Registrar Transação Aprovada: A transação aprovada é registrada no banco de dados.
-
Retorna Código "00": Se a transação foi aprovada, retorna o código "00" (aprovada).
-
Retorna Código "51": Se a transação foi rejeitada por falta de fundos, retorna o código "51".
*Esse fluxo representa o processo de aprovação, fallback e rejeição da transação com base nos saldos e MCC.
Com Locks Distribuídos
e Bloqueio Pessimista
, o processamento por account
é síncrono, mas operações distintas seguem simultâneas. O Redis
gerencia locks
para coordenar o acesso eficiente a recursos e o Redis Keyspace Notifications
, provê unlocks
através de Pub/Sub. Consulte a ADR
0003: gRPC e Redis Keyspace Notification reduzindo Latência e evitando Concorrência para maiores detalhes.
flowchart TD
A([▶️<br/>Recebe Transação JSON]) --> B[Inicia Processamento de Transação]
B --> C{Account da Transação está Bloqueado no <b>Lock Distribuído</b>?}
C -- Não --> D[🔐<br/><b>Bloqueia</b><br/>Account da Transação no Lock Distribuído]
D --> E[[Processa Autorização de Pagamento]]
C -- Sim --> M[✉️⬅️<br/><b>Subscreve</b><br/>Redis Keyspace Notification<br/><br/> ]
M --> R[⏸️<br/><b>Aguarda</b><br> receber Mensagem de desbloqueio da Account do Redis Keyspace Notification]
R --> N{Recebi Mensagem de desbloqueio em tempo útil? <br/> <b><i>t<i> < 100 ms</b>}
N -- Sim --> D
N -- Não --> O[❌<br/><b>Rejeitada</b><br/> Retorna Código <b>07</b> por Falha Genérica</b>]
E --> F{Ocorreu Erro no Processo da Transação?}
F -- Não --> G{Saldo é Suficiente?}
F -- Sim --> K[❌<br/><b>Rejeitada</b><br/> Retorna Código <b>07</b> por Falha Genérica</b>]
K --> J
G -- Sim --> H[Atualiza Saldo e Registra Transação Aprovada]
H --> I[✅<br/><b>Aprovada</b><br/> Retorna Código <b>00</b>]
I --> J[🔓<br/><b>Desbloqueia</b><br/> Account da Transação no <br> Lock Distribuído]
G -- Não --> L[❌<br/><b>Rejeitada</b><br/> Retorna Código <b>51</b> por Saldo Insuficiente</b>]
L --> J
J --> P[✉️➡️<br/><b>Publica</b><br/> mensagem de desbloqueio Redis Keyspace Notification]
P --> Q([⏹️<br/>Fim do Processo])
O --> Q
style D fill:#78771b,stroke:#000
style I fill:#009933,stroke:#000
style L fill:#cc0000,stroke:#000
style K fill:#cc0000,stroke:#000
style O fill:#cc0000,stroke:#000
style M fill:#007bff,stroke:#000
style R fill:#007bff,stroke:#000
style J fill:#78771b,stroke:#000
style P fill:#007bff,stroke:#000
*A etapa Processa Autorização de Pagamento
é uma sub-rotina do fluxo de Autorização, simplificada para sentido isolado. Detalhes do débito de saldos estão no fluxograma vinculado.
O diagrama de fluxo acima foi produzido após uma sessão de Miro Board conduzida pelos proponentes do desafio. O diagrama Miro da proposta de arquitetura, resultado dessa sessão, pode ser visto Aqui
A partir desse diagrama, construí uma segunda versão com poucas modificações, acrescentando detalhes e contexto para os que não estiveram presentes nessa sessão. Esse diagrama gerou o ADR 0003: gRPC e Redis Keyspace Notification reduzindo Latência e evitando Concorrência, visando nortear a implementação do requisito L4 neste projeto, com finalidade estritamente de treinamento.Via de regra, o que foi discutido naquela reunião deve ser implementado.
- 0001: Registro de Decisões de Arquitetura (ADR)
- 0002: Go, Gin, Gorm e PostgreSQL com Arquitetura Hexagonal e TDD
- 0003: gRPC e Redis Keyspace Notification reduzindo Latência e evitando Concorrência
- 0004: Banco Relacional Modelado Inspirado em Eventos
- 0005: Estratégia de Testes de Carga e Performance com Cliente Sintético
- 0006: Observabilidade com Prometheus e Grafana
- 0007: Tabela Auxiliar para melhoria de Performance
As tags de versões estão sendo criadas manualmente a medida que o projeto avança com melhorias notáveis. Cada funcionalidade é desenvolvida em uma branch a parte (Branch Based, feature branch) quando finalizadas é gerada tag e mergeadas em master.
Para obter mais informações, consulte o Histórico de Versões.
-
Linguagem:
-
Framework & Libs:
-
Infra & Tecnologias
-
GUIs:
- Swagger
- Github Project - Kanban
- Semantic Versioning 2.0.0
- Conventional Commits
- Keep a Changelog
- ADR - Architecture Decision Records
- Mermaid Diagrams
- Miro Diagrams
- Dockerized Load Testing Gatling
- Observability Metrics RED with Gatling
A figura do cabeçalho nesta página foi criada com a ajuda de inteligência artificial e um mínimo de retoques e construção no Gimp
Os seguintes prompts foram usados para criação no Bing IA:
Gopher caixa de mercado
"gopher azul, simbolo da linguagem golang com um bone laranja, trabalhando como caixa de supermercado com algumas maquinhas de cartão de credito e cartões em cima da mesa, estilo cartoon, historia em quadrinhos, fundo branco chapado para facilitar remoção"(sic)IA também é utilizada em minhas pesquisas e estudos como ferramenta de apoio; no entanto, artes e desenvolvimento são, acima de tudo, atividades criativas humanas. Valorize as pessoas!
Contrate artistas para projetos comerciais ou mais elaborados e aprenda a ser engenhoso!
-
Adotei o modelo hexagonal por sua flexibilidade com
ports
eadapters
, permitindo suporte aHTTP
,gRPC
e fácil extensão paraMensagens
oupub/sub
para atender ao requisitoL4
, sem impacto nocore
e com responsabilidades bem separadas. -
Para o
L4
, filas foram descartadas pelo proponente noMiro Board
devido à latência. Detalhes noADR
0003: gRPC e Redis Keyspace Notification e noKanban
. -
Refatoração das tabelas centralizou
transactions
e atualização de saldos porcategories
, garantindoimutabilidade
e mitigando inconsistências. Detalhes noADR
0004: Banco Relacional Modelado Inspirado em Eventos. -
Testes de performance com
Gatling
foram criados para garantir implantações seguras. Detalhes noADR
0005: Estratégia de Testes de Carga e Performance com Cliente Sintético. -
Adicionada
Observabilidade Métricas RED
usandoPrometheus
eGrafana
. Essas ferramentas também são úteis no desenvolvimento, quando usadas em conjunto aos testes dePerformance
eCarga
citadas anteriormente. Detalhes noADR
0006: Observabilidade com Prometheus e Grafana. -
Os números de métricas entre os testes do
Gatling
e doGrafana
(WIP) estão descolados (não de maneira muito significativa). É necessário maior investigação para entender os motivos. Também é esperado que, ao se adotar oCliente Sintético
K6
, pertencente ao ecossistemaGrafana
, esse descolamento deixe de ocorrer. -
Teste de stress abaixo foi realizado em um Notebook ROG Strix G16 - 13ª Geração após melhorias no banco e consultas para
Performance
. Os resultados podem variar dependendo das configurações e processos abertos na máquina de desenvolvimento. Detalhes noADR
0007: Tabela Auxiliar para Melhoria de Performance. -
O requisito
L4
foi aprimorado em suaConcorrência
com oRefactor
doadapter
dePubSub
Redis
e dorepository
dememoryLock
. A instância da aplicação sobrescreve noPubSub
uma única vez e distribui as mensagens de desbloqueio apenas para asrequests
bloqueadas, através dechannels
combufferSize
de1
, armazenados em umsyncMap
com chaveaccountUID
. Esse map, por sua vez, armazena oschannels
desubscriptions
em um outrosyncMap
cujas chaves sãotransactionUID
, tornando cada requisição única e eliminando conexões custosas com oRedis
. Com isso, espera-se validar a hipótese de que múltiplas instâncias são capazes de gerenciar os recursos de forma mais eficiente, resultando no aumento doTPS
atendido.
// Exemplo ilustrativo de estrutura sync.Map usada no sistema para gerenciar concorrência
subscriptions := sync.Map{}
// Criando um sync.Map para "account1"
account1Transactions := &sync.Map{}
account1Transactions.Store("transaction1", make(chan string, 1))
account1Transactions.Store("transaction2", make(chan string, 1))
subscriptions.Store("account1", account1Transactions)
// Criando um sync.Map para "account2"
account2Transactions := &sync.Map{}
account2Transactions.Store("transaction3", make(chan string, 1))
subscriptions.Store("account2", account2Transactions)
-
Aprimoramentos na
Observabilidade
com painéis de monitoramentoGrafana
doRedis
PubSub
eCache
fazem-se necessários, bem como melhorias na criação e no gerenciamento deLogs
(Grafana Loki
) eTraces
baseados emOpenTelemetry
comJaeger
. Esses aprimoramentos devem ser considerados no futuro próximo. -
Aprimorar o
Disaster Recovery
e implementarGracefull Shutdown
-
Testes adicionais devem ser criados (Concorrência e múltiplos cenários de erros nas rotas e serviços).
Este desafio me permite consolidar conhecimentos e identificar pontos cegos para aprimoramento. Continuarei trabalhando para evoluir o projeto e expandir minhas habilidades.
"Lifelong Learning & Prosper"
Mr. Spock, maybe 🖖🏾🚀