This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
This project uses third-party libraries under their own respective licenses. No modification of third-party licenses is intended.
The majority of components are created with terraform. See aws/terraform for more information.
The Ledger system follows a gateway-centric architecture, where the Ledger Gateway acts as the single entry point for the front-end and orchestrates communication with internal backend services.
The system is composed of:
- A front-end client (Angular + NGINX)
- A backend gateway (Ledger Gateway)
- Multiple backend services (SimpleAuth, Financial Service, User API)
- Asynchronous messaging infrastructure (Outbox Pattern + Kafka)
- Workers responsible for message relay and processing
All backend services are implemented in C# using .NET and ASP.NET Core.
The front-end client is built using Angular and is deployed behind NGINX, which plays two roles:
-
Static Content Server NGINX serves HTML, JavaScript, CSS, and other front-end assets.
-
Reverse Proxy NGINX proxies HTTP requests from the browser to the Ledger Gateway.
The front-end communicates exclusively via HTTP REST with the Ledger Gateway. It never accesses backend services directly.
The Ledger Gateway is the central coordination layer between the front-end and backend services.
- Acts as the single entry point for the front-end
- Handles authentication and authorization
- Validates JWT tokens issued by SimpleAuth
- Forwards requests to backend services
- Authenticates itself to backend services using API Keys
The Ledger Gateway communicates with backend services using multiple protocols, depending on the use case:
| Target Service | Protocol |
|---|---|
| SimpleAuth | REST |
| Financial Service | gRPC |
| User API | REST |
Authentication between the front-end and the Ledger Gateway is handled via JWT tokens, which can be transmitted in two ways:
- HTTP Cookies
- Authorization headers (Digest / Bearer tokens)
When a user registers or logs in:
- The front-end sends a request to the Ledger Gateway.
- The Ledger Gateway forwards the request to SimpleAuth.
- SimpleAuth issues a JWT containing the required claims.
- The Ledger Gateway returns the token to the front-end (typically via cookies).
The Ledger Gateway also validates JWTs locally, ensuring:
- The token was issued by SimpleAuth
- The token is valid and not expired
- The request is authorized before being forwarded
Internal service communication is authenticated using API Keys.
Each backend service has:
- Its own API key
- Validation logic for incoming requests
The Ledger Gateway authenticates itself to each backend service using the corresponding API key.
The SimpleAuth service is responsible only for authentication concerns.
- User authentication
- JWT token issuance
- Secure storage of authentication data
- Uses PostgreSQL as its database
- Uses Entity Framework (Code-First) for schema management
- Database schema is initialized via migrations
- Stores authentication-related data only (no user profile or financial data)
When a user is registered:
- The event is recorded in an OutboxMessages table
- This event is later picked up by the EventRelayWorker
- The event is published to Kafka so other services can react asynchronously
The Financial Service is responsible for all financial operations in the system.
- Money transfers between accounts
- Deposits (using a system “cash-in” account)
- Balance management
- Financial consistency
- Exposes a gRPC API, chosen to efficiently handle high-throughput and low-latency requests
- Has its own PostgreSQL database
- Maintains financial data such as balances and transactions
- Includes a Worker responsible for consuming Kafka messages
When a UserRegistered event is received:
- The Financial Service creates a financial account/balance for the user.
- A new event (
AccountBalanceCreated) is written to its Outbox table. - This event is later relayed to Kafka by the EventRelayWorker.
- Other services (such as User API) can consume and react to this event.
The User API manages general user profile data, not authentication data.
- Store and manage user profile and domain data
- Maintain the user lifecycle state
- Decide whether a user is considered active
- Has its own PostgreSQL database
- Contains a Worker that consumes Kafka events
A user is only considered active when both of the following are true:
- The user exists in the User API domain
- A corresponding financial account exists in the Financial Service
To handle this:
-
The User API maintains a state table tracking whether:
- The user profile has been created
- The financial account has been created
-
The Worker updates this table as Kafka events are processed
-
Once both conditions are satisfied, the user is marked as active
Inactive users cannot access the application
Each service maintains its own OutboxMessages table.
- Business operations and outbox writes occur in the same database transaction
- Guarantees consistency between state changes and emitted events
The EventRelayWorker is a shared infrastructure component responsible for:
- Reading OutboxMessages from service databases
- Publishing events to Kafka
- Handling retries and transient failures
Duplicate message publication is possible, but safe.
All consumers are designed to be idempotent:
- Each service maintains idempotency tables
- Duplicate Kafka messages do not cause duplicated side effects
Kafka is used as the system’s event backbone.
- Supports multiple independent consumers
- Enables event-driven integration between services
- Horizontally scalable
- Well-suited for high-volume, distributed systems
Kafka decouples services and allows the system to evolve without tight runtime dependencies.
- Docker version 20.10 or higher
- Docker Compose version 2.0 or higher (included in Docker Desktop)
- Signoz for observability and telemetry collection
- From the project root directory, run:
docker compose upThis will start all core services:
- PostgreSQL database (port 5432)
- Simple Auth API (port 5001)
- User API (port 5002)
- Financial Service (port 5003)
- Event Relay Worker (port 5005)
- Apache Kafka broker (port 29092)
- Kafka UI (port 8081)
- Front-end application (port 5006)
- Ledger Gateway (port 5000)
To enable distributed tracing and metrics collection:
- Create a shared Docker network:
docker network create signoz-ledger-net- Configure Signoz to use the shared network:
- Navigate to your Signoz installation:
signoz/deploy/docker/docker-compose.yaml - Add the network configuration:
- Navigate to your Signoz installation:
networks:
signoz-ledger-net:
external: true- Add
signoz-ledger-netto the x-common networks section
-
Start Signoz according to its documentation
-
Start the Ledger services (they are already configured to connect to
http://signoz-otel-collector:4317) -
Access the Signoz UI at
http://localhost:8080to view traces, metrics, and logs
- Docker Desktop (includes Docker, Docker Compose and Kubernetes)
- Bash interpreter
- Helm version 4.0.4 >=
- Signoz with Helm for observability and telemetry collection. Follow instructions or see steps bellow
- Build images with either
docker build -f <Dockerfile>on all services or build them withdocker compose up --build - Also, since this project allow you to also deploy to AWS, it may be necessary to adjust k8s/infra.yaml files on each service to pull images locally.
- From the project root directory, run:
./k8s/startup.shThis script applies all Kubernetes manifests (databases, brokers, services and apps) in the correct order.
This will start all core services:
- PostgreSQL database
- Simple Auth API
- User API
- Financial Service
- Event Relay Worker
- Apache Kafka broker
- Kafka UI
- Front-end application
- Ledger Gateway
- From the project root directory, run:
./k8s/teardown.shTo enable distributed tracing and metrics collection:
- Run the following instructions from your terminal
helm repo add signoz https://charts.signoz.io
helm repo update
helm pull signoz/signoz --untar
cd signoz
helm install signoz .
-
Run
kubectl port-forward -n <namespace> svc/signoz 8080:8080. If not specified in signoz installation, the <namespace> will be default -
Start the Ledger services (they are already configured to connect to
http://signoz-otel-collector:4317) -
Access the Signoz UI at
http://localhost:8080to view traces, metrics, and logs






