Stage 7 · Master
Phase 14 — Notification Service
Deployment
Ship one image with two entrypoints — API and worker — so the request-serving process and the delivery workers scale on independent schedules from a single build.
The API and the Worker Are Different Processes, Same Codebase
notification-service's HTTP API accepts send requests and returns quickly; its worker claims and delivers jobs continuously. They scale for different reasons — API replicas track request volume, worker replicas track queue depth — so they run as two separate Kubernetes Deployments, built from the same Docker image with a different command.
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.work go.work.sum ./
COPY pkg ./pkg
COPY services/notification-service ./services/notification-service
RUN go build -o /out/api ./services/notification-service/cmd/api
RUN go build -o /out/worker ./services/notification-service/cmd/worker
FROM alpine:3.20
RUN adduser -D -H meridian
COPY --from=build /out/api /usr/local/bin/notification-api
COPY --from=build /out/worker /usr/local/bin/notification-worker
COPY services/notification-service/migrations /migrations
USER meridianNo ENTRYPOINT is set here on purpose — each Kubernetes Deployment below supplies its own command against the same image, so a single build artifact serves both roles.
Different Replica Counts, Different Scaling Signals
image:
repository: registry.meridian.internal/notification-service
tag: ""
api:
replicaCount: 2
command: ["notification-api"]
resources:
requests: { cpu: 100m, memory: 128Mi }
worker:
replicaCount: 4
command: ["notification-worker"]
resources:
requests: { cpu: 200m, memory: 128Mi }
autoscaling:
enabled: true
minReplicas: 4
maxReplicas: 16
metric: notification_jobs_pending # a custom metric, wired in the Monitoring lesson
env:
- name: NOTIFICATION_SERVICE_DB_URL
valueFrom:
secretKeyRef: { name: notification-service-db, key: url }
- name: EMAIL_PROVIDER_API_KEY
valueFrom:
secretKeyRef: { name: notification-providers, key: email-api-key }
- name: SMS_PROVIDER_API_KEY
valueFrom:
secretKeyRef: { name: notification-providers, key: sms-api-key }Autoscaling the worker on notification_jobs_pending — rather than CPU — means replica count tracks the actual backlog a resident is waiting on, not an indirect proxy for it.
Provider Credentials Never Enter the Image
The same image tag is promoted from staging to production unchanged; only the Secret objects it reads from differ per environment. Baking an API key into the image would mean every environment shares one credential and rotating it requires a rebuild instead of a Secret update.
Email and SMS provider keys are typically billed per-send. Treat them with the same rotation discipline as a database credential — a compromised key does not just leak data, it can run up cost until noticed.
Confirm the API and Worker Both Roll Out Cleanly
Applied exercise
Separate worker replica counts per channel
Push delivery is nearly instant but SMS delivery is comparatively slow, and the current single worker pool processes both channels together, letting a slow SMS batch delay push delivery.
- Decide whether to split into two worker commands (one per channel) or add channel-aware concurrency inside one worker process.
- Update the Helm values to reflect whichever design you chose.
- State how this changes the Claim query or its batchSize parameter, if at all.
- Identify one new failure mode this split introduces that the single pool did not have.
Deliverable
An updated deployment design (values.yaml changes or a described code change) plus the identified new failure mode.
Completion checks
- The design explains why the chosen split actually decouples push latency from SMS latency.
- The new failure mode is named specifically, not just 'more complexity'.
Why do the API and worker share one Docker image instead of each having its own Dockerfile?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.