- Sobre o sistema
- Arquitetura proposta
- Como os sistemas funcionam
- Arquitetura local
- Requisitos mínimos
- Executando as aplicações
- Testes de carga
- Arquitetura interna dos serviços
- Desenvolvimento
- Documentação dos endpoints
- Aprimoramentos futuros
Registra operações de lançamentos (débitos e créditos) de comerciantes e gera um saldo diário consolidado com o balanço total de todas as operações realizadas em cada dia.
A ideia é que o próprio comerciante registre operações de caixa e realize a consulta do saldo diário, através de um site ou aplicativo.
- Autenticação do comerciante
- Registro de lançamento de débito
- Registro de lançamento de crédito
- Consulta de saldo consolidado diário
Alguns fatores influenciaram nas decisões durante a modelagem do sistema:
- Um "lançamento" foi considerado como descrito na contabilidade. Como é o próprio comerciante que está registrando a operação, foi considerado que ele está usando uma conta ativa. Dessa forma: débitos aumentam o valor do caixa e créditos diminuem o valor do caixa. De qualquer forma o sistema foi projetado para tornar simples essa modificação, se necessário.
💡 Abra a imagem em nova guia para ampliar
Olhando de uma visão macro, o sistema como um todo funciona da seguinte forma:
- O comerciante teria acesso a uma aplicação frontend como um site ou app.
- O aplicativo se autenticaria através de um provedor de autenticação utilizando OpenId Connect. O provedor de autenticação poderia ser o Keycloak ou o Microsoft Entra ID, por exemplo.
- Após se autenticar, a aplicação faria uma requisição para o API Gateway (nesse caso representado pelo kong). O API Gateway valida o token de acesso através do provedor de autenticação e repassa a requisição para os sistemas internos.
- O sistema interno recebe a solicitação do API Gateway, processa e responde de volta para o API Gateway.
- Por sua vez, o API Gateway repassa a resposta, até chegar ao comerciante.
💡 Abra a imagem em nova guia para ampliar
- Escalabilidade:
- Apesar de fazer parte de um sistema único, ele foi dividido em partes independetes para que possam ser facilmente escaláveis. Neste caso, as APIs serão escaladas pelo throughput de requisições HTTP e o worker será escalado pelo throughput de mensagens recebidas.
- O banco de dados separado das duas aplicações também permite escalabilidade independente.
- Resiliência:
- Os sistemas são independentes. Se uma API falhar, a outra não falha, e vice versa.
- Se o worker falhar, as APIs não falham e, ao mesmo tempo, as mensagens também não são perdidas, ficam retidas na fila para que sejam consumidas logo quando o consumidor retomar o processamento.
- Disponibilidade:
- A arquitetura foi projetada principalmente para manter uma alta disponibilidade.
- A API de saldo suporta um alto throughput pois recebe apenas uma requisição de consulta que é feita através de um banco de dados otimizado para leitura.
- A API de lançamentos recebe requisições otimizadas para escrita, onde operações são apenas adicionadas.
- O processamento do saldo é feito de forma assíncrona e não interrompe nenhum fluxo. É usada uma abordagem de concorrência otimista, portanto existe apenas uma operação de lock no saldo de apenas um dia (apenas uma linha da tabela do banco de dados) por um período de poucos milissegundos.
- Segurança:
- O sistema como um todo possui apenas uma porta de entrada, que possui auntenticação e pode ser facilmente extensível para suportar rate limit.
- Os sistemas da rede privada ficam inacessíveis externamente. Por conta disso, não há necessidade de revalidar os tokens de acesso já previamente validados e, além disso, não existe obrigatóriedade de se usar HTTPS, que aumentaria latência.
- Monitoramento:
- O API Gateway gera e repassa um Correlation Identifier para os sistemas. Com isso seria possível instrumentar ferramentas de observalidade para realizar o tracing distribuído.
Algumas considerações importantes sobre a arquitetura:
- Apesar de os sistemas terem sido projetados para estarem separados preventivamente, esta arquitetura pode não ser a mais eficiente com relação a custo. Existem aplicações que suportam praticamente mais de 50 requisições por segundo usando apenas um banco de dados e realizando vários processos de consulta, escrita e processamento de mensagens ao mesmo tempo. Por isso é sempre interessante medir o quanto de processamento é suportado e os custos necessários.
- O banco de dados PostgreSQL foi selecionado porque ele é otimizado tanto para escrita e leitura. Além disso, ele foi escolhido principalmente pela alta disponibilidade.
- O banco de dados da API de saldo pode ter inconsistência eventual por conta do processamento assíncrono, apesar de que, por ser um consolidado diário, não causaria impactos para comerciante.
- Nesse primeiro momento não houve necessidade de usar uma ferramenta de cache. Se fosse necessário, seria interessante experimentar um cache "na borda" como um cache HTTP para consulta de saldo (saldos de dias passados não serão alterados com frequência).
Fluxo de como os sistemas funcionam e interagem entre si de forma mais detalhada:
💡 Abra a imagem em nova guia para ampliar
Cada serviço consome seu próprio banco de dados. Por ser um sistema simples, existem apenas duas entidades armazenadas:
Faz parte do serviço accounting-operations
.
Coluna | Tipo | Descrição |
---|---|---|
merchant_id |
texto | Um identificador único do comerciante. |
registered_at |
data e tempo | Data e tempo do registro de lançamento, em UTC. |
value |
monetário | Valor do lançamento. |
type |
inteiro | Identificador do tipo de lançamento (e.g. débito ou crédito). |
As colunas merchant_id
e registered_at
formam um identificador único composto.
Faz parte do serviço daily-balances
.
Coluna | Tipo | Descrição |
---|---|---|
merchant_id |
texto | Um identificador único do comerciante. |
day |
data | Data (dia). |
total |
monetário | Valor total do saldo. |
As colunas merchant_id
e day
formam um identificador único composto.
A arquitetura local (para fins de teste) é muito semelhante com a arquitetura proposta, mas com algumas pequenas limitações.
- API Gateway e autenticação: Kong.
- Banco de dados: PostgreSQL.
- Aplicações: .NET.
- Broker de mensageria: RabbitMQ.
- O API Gateway foi implementado com o Kong Open Source. Por conta disso, ele não suporta ferramentas da licença Enterprise, como OpenId Connect. Ou seja, ele não tem suporte para provedores como o Keycloak, por exemplo.
- Para autenticação é usado o próprio Kong, com uma combinação de um sistema simples e utilitário para geração de tokens de acesso válidos.
Requisitos mínimos para executar as aplicações:
Após garantir que você possui os requisitos mínimos, apenas execute o comando abaixo para iniciar as aplicações:
docker compose up -d
💡 As aplicações são inicializadas na sequência apropriada automaticamente.
⚠️ Observação: dependendo do sistema, é possível que as portas definidas nodocker-compose
já estejam em uso. Nesse caso, será necessário ajustá-las ou, potencialmente, parar as aplicações que utilizam estas portas.
Veja a documentação dos endpoints para saber como realizar as requisições e quais são os retornos possíveis.
Lembrando que também é possível fazer requisições diretamente pelo Visual Studio Code se estiver usando a extensão REST Client, acessando o arquivo Apis.http.
Os testes de carga validam alguns requisitos não-funcionais do sistema. As requisições são feitas a partir do API Gateway (fluxo completo). Atualmente o seguinte cenário é suportado:
- Consulta de saldo: 50 requisições simultâneas por segundo com taxa de falha menor que 5%.
Para os testes de carga é utilizado o Grafana K6.
🚧 Os testes de carga ainda não chegam perto de um cenário real. Faltam outros cenários de testes mais bem elaborados.
Para executar os testes de carga, utilize o comando abaixo:
docker compose -f docker-compose-test.yml up
Infelizmente ainda não há suporte para visualizar os testes de forma amigável a não ser pelos logs. Futuramente poderá ser incluído.
⚠️ Observação: O arquivodocker-compose-test
estende o arquivo padrão, portanto podem ser aplicadas as mesmas observações da seção executando as aplicações.
Caso tenha executado os testes no modo detached (-d
), ainda é possível observar os logs com os resultados dos testes através do comando abaixo:
docker compose -f docker-compose-test.yml logs load-tests
Os serviços (backend) possuem uma mistura de arquitetura hexagonal, arquitetura limpa e utiliza a mesma estrutura do CQRS. Dessa forma as aplicações são estruturadas da seguinte forma:
💡 Abra a imagem em nova guia para ampliar
Apesar de parecer complexo, o fluxo é simples e acaba se tornando intuitivo para adicionar novas funcionalidades ou alterar uma funcionalidade já existente. O fluxo consiste em:
- A aplicação recebe um payload de entrada, que podem ser tanto uma requisição HTTP quanto uma mensagem.
- O payload de entrada é recebido:
- No caso das requisições HTTP, é recebido por um controller (convertido automaticamente).
- No caso de mensagens, é recebido por um consumidor (convertido manualmente).
- O payload passa por uma validação (no caso dos controllers a validação ocorre automaticamente).
- O payload é convertido para um command ou uma query dependendo da natureza da solicitação.
- No caso do consumidores, o payload sempre será convertido para um command.
- O command ou query são enviados para um mediator que envia para um command handler ou query handler equivalente.
- Os command handlers e query handlers executam o processamento utilizando serviços de domínio, entidades de domínio e repositórios para acesso e alteração de dados.
- O command geralmente é convertido para uma entidade de domínio.
- No final do processamento:
- O command handler publica um evento (que pode ser de domínio ou de integração).
- O query handler trata os dados e retorna um output (que será retornado como resposta).
Algumas orientações de como preparar o ambiente de desenvolvimento.
- Será necessário configurar os requisitos mínimos.
- .NET SDK versão 8.
- Node JS (apenas obrigatório para o desenvolvimento de testes de carga).
- Uma IDE como Visual Studio Code ou Visual Studio.
Extensões recomendadas se estiver usando o Visual Studio Code:
- EditorConfig: Formata os arquivos de acordo com as regras. É muito importante que tenha instalada esta extensão.
- Draw.io Integration: Permite editar arquivos
.drawio.png
ou.drawio.svg
. - REST Client: Permite executar requisições através de arquivos
.http
. - C#: No caso de estar desenvolvendo em C# no Visual Studio Code.
Os bancos de dados de cada aplicação foram pré configurados no docker compose
:
- Para o banco "Accounting Operations", execute
docker compose up -d accounting-operations-db
. - Para o banco "Daily Balances", execute
docker compose up -d daily-balances-db
.
Cada serviço possui um CLI para executar migrations de banco de dados de forma independente. Para facilitar o uso, para cada banco de dados foi configurado um serviço no docker compose
:
- Para o banco "Accounting Operations", execute
docker compose up accounting-operations-migrate
. - Para o banco "Daily Balances", execute
docker compose up daily-balances-migrate
.
💡 Não é necessário executar as migrations via comando para executar os testes de integração. Os testes de integração executam as migrations automaticamente em um banco de dados separado.
Para iniciar o broker de mensageria, basta executar o comando docker compose up -d message-broker
.
As aplicações são separadas por contextos. No caso dos serviços, a separação é feita da seguinte forma:
- 📁 services
- 📁 accounting-operations
- 📁 daily-balances
- 📁 simple-auth
Cada service
possui sua própria solution. Se você estiver usando o Visual Studio terá de abrí-las separadamente. Casa esteja usando o Visual Studio Code, use o comando .NET Open Solution
para alternar entre uma solution e outra.
Estando dentro de cada serviço é possível iniciar cada aplicação e executar os testes.
Caso esteja desenvolvendo para este repositório é importante seguir as seguintes convenções:
- Crie uma branch com nome curto e descritivo do trabalho a ser feito. Inclua como prefixo o seu nome de usuário do github. Exemplo:
leandroslc/performance-improvement
. - Após concluir o trabalho na branch, dê um push e crie um Pull Request.
- O merge do Pull Request deve sempre ser
Rebase
.
Registra um débito. Este endpoint não trata registros duplicados.
-
Endpoint
Metódo Url POST http://localhost:5406/debit -
Parâmetros
Nome Tipo Local Descrição Authorization string Cabeçalho -- -
Corpo (JSON)
Nome Tipo Descrição registrationDate string Data e tempo de registro no formato ISO 8601, em UTC. value number Valor -
Respostas
- 204: Sucesso
- 400: Problemas de validação no formato RFC 7807.
- 500: Algum erro inesperado ou um registro exatamente igual já existe. By design, não é feita a verificação se o registro já existe antes de salvar.
Registra um crédito. Este endpoint não trata registros duplicados.
-
Endpoint
Metódo Url POST http://localhost:5406/credit -
Parâmetros
Nome Tipo Local Descrição Authorization string Cabeçalho -- -
Corpo (JSON)
Nome Tipo Descrição registrationDate string Data e tempo de registro no formato ISO 8601, em UTC. value number Valor -
Respostas
- 204: Sucesso
- 400: Problemas de validação no formato RFC 7807.
- 500: Algum erro inesperado ou um registro exatamente igual já existe. By design, não é feita a verificação se o registro já existe antes de salvar.
Consulta o saldo de um dia. Caso o dia não tenha tido algum lançamento, o saldo retornado será zero.
-
Endpoint
Metódo Url GET http://localhost:5406/balance -
Parâmetros
Nome Tipo Local Descrição Authorization string Cabeçalho -- Day string Query Dia para consulta no formato ISO 8601 ( YYYY-MM-DD
). -
Respostas
- 200: Sucesso, com o conteúdo:
-
{ "total": number }
-
- 400: Problemas de validação no formato RFC 7807.
- 500: Algum erro inesperado.
- 200: Sucesso, com o conteúdo:
Obtém um token de acesso para testes.
-
Endpoint
Metódo Url GET http://localhost:54062/v1/tokens -
Parâmetros
Nome Tipo Local Descrição Authorization string Cabeçalho -- UserId string Query Um id personalizado para o usuário de teste. Caso não seja especificado, será usado um id aleatório. -
Respostas
- 200: Sucesso, com conteúdo:
-
string
-
- 500: Algum erro inesperado. Por ser uma API simples para fins de teste, alguns erros esperados também são retornados como 500.
- 200: Sucesso, com conteúdo:
Algumas considerações sobre o que poderia ser melhorado:
- Poderia haver um exemplo funcional na nuvem, mas infelizmente não tenho uma conta que possa usar gratuitamente.
- Os testes de carga precisam ser melhorados para executarem um cenário mais realista, como por exemplo, executar registros simultâneos enquanto várias requisições são realizadas simultaneamente para consulta em um banco de dados com milhares de registros.
- Poderia haver uma aplicação frontend, mas infelizmente não tive tempo.
- Seria interessante adicionar observabilidade nas aplicações.
- Utilizar um API Gateway que suporte uma autenticação melhor.