Today I'm showcasing a project I've been working on for a while: Ledgerly, a simplified microservices reference project built with .NET and Angular.
I built Ledgerly to be be focused purely on microservices best practices.
What it demonstrates:
πΉ Database-per-service β 4 services, 4 independent databases
πΉ gRPC for synchronous service-to-service calls
πΉ RabbitMQ for async events β invoice status changes publish events that a dashboard service consumes to keep KPIs in sync, with zero direct coupling
πΉ API gateway + service discovery (Eureka locally/Docker, Kubernetes DNS in K8s)
πΉ JWT auth issued by its own service
It also ships as a full deployment guide β run it locally, in Docker Compose, or on Kubernetes, including a walkthrough for deploying the cluster to Azure (AKS).
Built as a boilerplate: fork it, swap in your own domain, keep the architecture.
Databases
Each of the four .NET services has its own SQL Server database β there is
no shared database. All four databases live on the host machine, even
when the services themselves run in Docker or Kubernetes β this keeps setup
simple (no containerized SQL Server to manage):
| Service | Database |
|---|---|
| Invoice service | ledgerly-invoice |
| Customer service | ledgerly-customer |
| Dashboard service | ledgerly-dashbaord |
| Auth service | ledgerly-auth |
How service discovery works
- Local / Docker: services register themselves with Eureka, and the Ocelot gateway resolves downstream services by name through Eureka.
- Kubernetes: Eureka is removed. Each service is exposed by a Kubernetes Service (ClusterIP), and the gateway routes directly to those service names. ClusterIP load-balances across pods.
Prerequisites
Install these on your host machine before running the project, in any mode:
-
JDK (Java 11+) β to build/run the Eureka server (
eureka-server/). Needed for local and Docker modes; Kubernetes doesn't use Eureka. - SQL Server Express β all four databases run on the host in every mode (local, Docker, and Kubernetes). See Databases above.
-
.NET 10 SDK (Developer Pack) β to build/run the services locally. Note:
ledgerly-backend-invoice-servicetargetsnet6.0, so the matching .NET 6 runtime is required alongside .NET 10. - RabbitMQ, via Chocolatey β needed for local mode only (Docker Compose and Kubernetes each run their own RabbitMQ container):
choco install rabbitmq -y
This pulls in Erlang as a dependency β restart the RabbitMQ service
afterwards so it picks up the Erlang install:
Restart-Service RabbitMQ
- Create the databases β run every script in db/ against your SQL Server instance (creates the four databases, schema, and seed data) before starting any service.
Running the project
You can run the stack in three modes.
1. Local (dotnet + Eureka)
Start the Eureka discovery server first, then launch all the .NET services and
the frontend with the batch script.
# 1. Start Eureka (in its own terminal; leave it running)
cd eureka-server
.\gradlew bootRun
# 2. From the repo root, build + start all services and the Angular frontend
run-all.bat
# run-all.bat nobuild # skip the build, just start everything
run-all.bat opens a separate window per service:
- invoice β
https://localhost:7052 - customer β
https://localhost:7099 - dashboardβ
https://localhost:7063 - auth β
https://localhost:7109 - gateway β
https://localhost:7019 - frontend β
http://localhost:4200
Connection string: each service reads
ConnectionStrings:DefaultConnection
from its ownappsettings.json(e.g.
ledgerly-backend-invoice-service/appsettings.json). Update theData Source,
User ID, andPasswordthere to match your local SQL Server Express
instance.
2. Docker Compose
Builds and runs every service β including Eureka β as containers on a shared
network. The database stays on the host machine (host.docker.internal\SQLEXPRESS).
# DB credentials come from a .env file (SQL_USER / SQL_PASSWORD)
docker-compose up --build
Exposed ports: frontend 4200, gateway 8080, invoice 5052, customer 5246,
dashboard 5208, auth 5109, Eureka 8761. Services wait for Eureka to be
healthy before starting.
Connection string: copy
.env.exampleto.envand setSQL_USER/
SQL_PASSWORD, plus one connection string per service βINVOICE_DB,
CUSTOMER_DB,DASHBOARD_DB,AUTH_DB. These are injected into each
container asConnectionStrings__DefaultConnection.
3. Kubernetes (Local + Deploy to AKS)
On Kubernetes Eureka is not used. Instead, each deployment is exposed by a
Kubernetes Service, and the API gateway routes traffic directly to those
services (Kubernetes DNS + ClusterIP handle discovery and load-balancing).
Every service has its own Deployment, so each can be scaled independently
for maximum scalability (with HPAs autoscaling on CPU).
A single Ingress is what allows connections into the Kubernetes cluster from
outside β it is the only public entry point. It routes by path, and the most
important entry point is /, which points to the Angular frontend (the
/InvoiceGW, /CustomerGW, /Dashboard, /AuthGW paths go to the gateway).
Connection string: copy
k8s/01-db-secret.example.yamlto
k8s/01-db-secret.yamland fill in theConnectionStrings__DefaultConnection
value in each of the four secrets. For local minikube testing, point at
host.minikube.internal,1433(not the\SQLEXPRESSnamed instance, and not
an AKS/cloud DB) β see the comments in the example file for the one-time SQL
Server Express config this requires.
kubectl apply -k k8s/
# route local traffic to the ingress controller (instead of using `minikube tunnel`)
kubectl port-forward -n ingress-nginx service/ingress-nginx-controller 8090:80
Then open the app at http://myapp.local:8090 (after mapping myapp.local
to the minikube IP in your hosts file β see the k8s README).
See k8s/README.md for full details β image builds, the DB
The projectπ: [https://lnkd.in/d_NeksRk]

Top comments (0)