Three services on Kubernetes, split across namespaces:
data for the stateful backends (Postgres, RabbitMQ),
dev and prod for the app.
Each service owns its Helm chart and deploys independently — same chart for both environments, with a values overlay carrying the differences.
ExternalName Services sit between the app and its infrastructure, so moving a database to RDS is a one-line change the app never sees.
Prod is a real boundary: restricted Pod Security, default-deny networking, resource quotas.
Argo CD reconciles everything from Git, so the cluster is whatever the repo says — and shipping to prod means committing a version, not running a deploy.
- JDK 21
- Node 24
- Docker
- DockerHub
- Github
- git
- kubectl
- helm
- minikube
- need the following if running locally:
- maven
- postgres
- rabbitmq
- argo cd cli
k8s-helm-event-driven-microservices/
├── docker-compose.yml # local dev: all services + rabbit + postgres
├── README.md
├── .github/workflows/ # ── all CI lives at the repo ROOT ──
│ ├── order-service.yml # path-filtered to order-service/**
│ ├── notification-service.yml # path-filtered to notification-service/**
│ └── ui.yml # path-filtered to ui/**
├── order-service/ # ── standalone microservice ──
│ ├── pom.xml
│ ├── mvnw, mvnw.cmd, .mvn/
│ ├── Dockerfile
│ ├── src/main/java/com/k8sdemo/order/...
│ ├── src/main/resources/
│ │ ├── application.yaml
│ │ ├── application-local.yaml
│ │ ├── application-dev.yaml
│ │ ├── application-prod.yaml
│ │ └── db/changelog/ # Liquibase changelogs
│ └── helm/ # its OWN chart
├── notification-service/ # ── standalone microservice ──
│ └── (same shape as order-service)
├── ui/ # ── standalone microservice ──
│ ├── package.json
│ ├── vite.config.ts
│ ├── src/
│ ├── Dockerfile # multi-stage: node build -> nginx
│ └── helm/
├── infra/ # ── cluster setup: raw manifests you kubectl apply ──
│ └── k8s/
│ ├── namespaces.yaml # dev / prod / data namespaces + labels
│ ├── data/ # backing services (stands in for RDS + Amazon MQ)
│ │ ├── orders-db/ # one Postgres per service PER ENV —
│ │ │ ├── dev.yaml # dev and prod never share a database
│ │ │ └── prod.yaml
│ │ ├── notifications-db/
│ │ │ ├── dev.yaml
│ │ │ └── prod.yaml
│ │ └── rabbitmq/ # official image + STOMP plugins
│ │ ├── dev.yaml
│ │ └── prod.yaml
│ ├── dev/ # dev-only cluster config
│ │ └── externalnames.yaml # ExternalName Services -> data tier
│ └── prod/ # prod-only cluster config
│ ├── externalnames.yaml
│ ├── network-policy.yaml # restrict cross-namespace traffic
│ └── resource-quota.yaml # cap prod namespace resources
└── argocd/ # Argo Applications (one per service × env)
├── order-service/
│ ├── dev.yaml
│ └── prod.yaml
├── notification-service/
│ ├── dev.yaml
│ └── prod.yaml
└── ui/
├── dev.yaml
└── prod.yaml
- Five moving parts, two clean boundaries. Read this diagram before writing a line of code — every later stage is just making one piece of it real.
┌───────────────┐
│ ui │
│ React + nginx │
└───────┬───────┘
│ REST (nginx proxies /api)
▼
┌──────────┼───────────────────────── minikube cluster ─────────────────────────────────┐
│ │ │
│ ┌───────▼───────────┐ ┌────────────────────────┐ │
│ │ order-service │ │ notification-service │ │
│ │ Spring Boot │ │ Spring Boot │ │
│ └───┬───────────┬───┘ └────┬──────────────┬────┘ │
│ │ │ │ │ │
│ │ │ ┌────────────────────┐ │ │ │
│ │ └───────▶│ RabbitMQ │◀──────────┘ │ │
│ │ │ topic exchange+DLQ │ │ │
│ │ └────────────────────┘ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────┐ ┌────────────────────────┐ │
│ │ orders-db │ │ notifications-db │ │
│ │ Postgres (data) │ │ Postgres (data) │ │
│ └───────────────────┘ └────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────┘
PENDING ──▶ OrderCreated ──▶ SENT ──▶ NotificationSent ──▶ NOTIFIED
orders-db / notifications-db / rabbitmq are ExternalName aliases. Each
namespace resolves them to its own backend: dev → *-dev, prod → *-prod.
order-service — Spring Boot REST API. Accepts orders, persists to its own
Postgres, publishes OrderCreated to RabbitMQ, listens for NotificationSent.
notification-service — Spring Boot consumer. Listens for OrderCreated,
records a notification in its own Postgres, publishes NotificationSent.
ui — React/TypeScript (Vite) served by nginx, which proxies /api to
order-service by Kubernetes DNS name.
- This is the sequence every later stage exists to support. Four events, two services, zero direct calls between them.
order-service RabbitMQ notification-service
│ │ │
POST /orders │ │
│ save PENDING │ │
├────────────────────▶│ OrderCreated │
│ ├──────────────────────▶│
│ │ save SENT
│ │◀──────────────────────┤
│ NotificationSent │ │
│◀────────────────────┤ │
save NOTIFIED │ │
│ │ │
An order starts PENDING. OrderCreated goes on the broker.
notification-service picks it up, does its work, emits NotificationSent.
order-service hears that and flips the order to NOTIFIED.
No orchestrator. No service calling another service. Each one reacts to events and owns its own decision — that's a choreographed saga.
Two properties fall out of it:
- Database-per-service. Each service owns its own Postgres — and each
environment owns its own pair, so
dataholds four. No shared tables anywhere. If two services shared a database they'd be one deployable pretending to be two; if dev and prod shared one, a test fixture would be production data. - No direct calls. order-service doesn't know notification-service exists. Kill the consumer, place an order — the order still succeeds, and the notification happens whenever it comes back. The broker is all they share.
That's what makes them independently deployable, and why each owns its own Helm chart.
- start
minikube start --cpus=4 --memory=7000 --cni=calico --kubernetes-version=stable--cni=calico: minikube's default CNI ignores NetworkPolicies — without this,prod/network-policy.yamlapplies cleanly and enforces nothing
- addons
minikube addons enable ingress: nginx ingress controller for the web UIminikube addons enable metrics-server: for kubectl top + HPA later
- Create Namespaces (dev/prod/data)
k apply -f infra/k8s/namespaces.yaml- verify:
k get namespace
- Create Data Tier (data)
k apply -R -f infra/k8s/data/- verify:
k -n data get pods,svc,pvc
- Create ExternalNames (dev/prod → data)
k apply -f infra/k8s/dev/externalnames.yamlk apply -f infra/k8s/prod/externalnames.yaml- verify:
kubectl -n <namespace> get svc
- (Prod Only) - Create Prod-only policies
k apply -f infra/k8s/prod/network-policy.yamlk apply -f infra/k8s/prod/resource-quota.yaml
- Create Services In Argo
IMPORTANT see Argo's section if you have not set it up yet
k apply -R -f argocd/- verify:
kubectl -n argocd get applications - deploys all six apps - dev and prod (three services each)
- Verify Everything is Configured
k -n <namespace> get pods,svc,pvc
- start apps
docker composeorlocally
- ui
localhost:3000
- order-service
localhost:8080
- notification-service
localhost:8081
- postgres databases:
notificationsorders
- rabbitMQ dashboard:
- http://localhost:15672/#/
- guest/guest
- ui
k -n dev port-forward svc/ui 8081:8080
- order-service
k -n dev port-forward svc/order-service 8080:8080
- notification-service
k -n dev port-forward svc/notification-service 8082:8080
- argo ui
k port-forward -n argocd svc/argocd-server 8090:443
- grafana
k -n monitoring port-forward svc/monitoring-grafana 3001:80
- DEV
- Data Tiers
k apply -f infra/k8s/data/orders-db/dev.yamlk apply -f infra/k8s/data/notifications-db/dev.yamlk apply -f infra/k8s/data/rabbitmq/dev.yaml
- ExternalNames
k apply -f infra/k8s/dev/externalnames.yaml
- Argo
k apply -f argocd/order-service/dev.yaml -f argocd/notification-service/dev.yaml -f argocd/ui/dev.yaml
- Data Tiers
- PROD
- Data Tiers
k apply -f infra/k8s/data/orders-db/prod.yamlk apply -f infra/k8s/data/notifications-db/prod.yamlk apply -f infra/k8s/data/rabbitmq/prod.yaml
- ExternalNames
k apply -f infra/k8s/prod/externalnames.yaml
- Argo
k apply -f argocd/order-service/prod.yaml -f argocd/notification-service/prod.yaml -f argocd/ui/prod.yaml
- Data Tiers
- point Docker at minikube's daemon so images are visible to the cluster:
eval $(minikube docker-env)
- Build Docker Files
docker build -t order-service:0.1.0 ./order-servicedocker build -t notification-service:0.1.0 ./notification-servicedocker build -t ui:0.1.0 ./ui
- build both service JARs (ui's builds itself):
mvn -f order-service/pom.xml clean packagemvn -f notification-service/pom.xml clean package
- start up:
docker compose up --build
helm install order-service ./order-service/helm -n devhelm install notification-service ./notification-service/helm -n devhelm install ui ./ui/helm -n dev- verify:
k -n dev get pods
Same charts, values-prod.yaml layered on top. The prod overlays pin image
SHAs — override with --set image.tag=latest if those aren't in your registry.
helm install order-service ./order-service/helm -n prod -f ./order-service/helm/values-prod.yamlhelm install notification-service ./notification-service/helm -n prod -f ./notification-service/helm/values-prod.yamlhelm install ui ./ui/helm -n prod -f ./ui/helm/values-prod.yaml
- render without applying:
helm template order-service ./order-service/helm - with the prod overlay:
helm template order-service ./order-service/helm -f ./order-service/helm/values-prod.yaml - dry run against the cluster:
helm install order-service ./order-service/helm -n dev --dry-run - lint:
helm lint ./order-service/helm
helm upgrade order-service ./order-service/helm -n dev- create-or-update in one command:
helm upgrade --install order-service ./order-service/helm -n dev - wait for healthy, roll back on failure:
helm upgrade --install order-service ./order-service/helm -n dev --wait --atomic
helm list -n devhelm get values order-service -n devhelm get manifest order-service -n devhelm history order-service -n dev
helm rollback order-service -n dev# previous revisionhelm rollback order-service 3 -n dev# a specific one
helm uninstall order-service notification-service ui -n dev
- create namespace
k create namespace argocd
- install Argo CD
k apply --server-side -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml--server-side: without it, the ApplicationSet CRD exceeds kubectl's 256KB annotation limit and silently fails to install
- wait for pods:
k -n argocd get pods -w
- forward-port
kubectl port-forward -n argocd svc/argocd-server 8090:443
- UI: https://localhost:8090
- user: admin
- password:
k -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo
- commit the argocd manifests
- apply
k apply -R -f argocd/-R: recurses into the per-service subdirectories
- verify
k -n argocd get applications
# Delete the Argo Applications - Argo cascades, removes the Pods, Services, and Secrets it deployed
k delete -R -f argocd/
# Delete backends - they live in the `data` namespace
k delete -R -f infra/k8s/data/
# Delete Persistent Volume - PVCs outlive their StatefulSet by design, data is supposed to survive the workloads
k -n data delete pvc --all
# Delete namespaces
k delete namespace dev prod
# Argo stays up. To remove it too:
k delete namespace argocd
# no dev, no prod (data remains unless you delete it)
k get ns
# only kube-system and ingress-nginx
k get pods -A
# empty
k get pv - DEV
# Delete namespace - order-service, notification-service, ui (pods, Services, Secrets)
kubectl delete -f argocd/order-service/dev.yaml \
-f argocd/notification-service/dev.yaml \
-f argocd/ui/dev.yaml
# Delete backends - they live in the `data` namespace
k delete -f infra/k8s/data/orders-db/dev.yaml \
-f infra/k8s/data/notifications-db/dev.yaml \
-f infra/k8s/data/rabbitmq/dev.yaml
kubectl -n data delete pvc -l app=orders-db-dev
kubectl -n data delete pvc -l app=notifications-db-dev
kubectl -n data delete pvc -l app=rabbitmq-dev
kubectl delete namespace dev
- PROD
kubectl delete -f argocd/order-service/prod.yaml -f argocd/notification-service/prod.yaml -f argocd/ui/prod.yaml
kubectl delete -f infra/k8s/data/orders-db/prod.yaml -f infra/k8s/data/notifications-db/prod.yaml -f infra/k8s/data/rabbitmq/prod.yaml
kubectl -n data delete pvc -l app=orders-db-prod
kubectl -n data delete pvc -l app=notifications-db-prod
kubectl -n data delete pvc -l app=rabbitmq-prod
kubectl delete namespace prod
- run:
./latest-dockerhub-tags.sh - Prints the newest image tag on Docker Hub for each service next to what
values-prod.yamlpins, so you don't have to check three repos by hand. - use flag
--bumprewrites the values files to the newest tags — review the diff, commit, and Argo deploys.