RS
Ravi Shukla
HomeBlogToolsAbout
Resume
RS
Ravi Shukla

Senior Java + AI engineer. Kafka, RAG, distributed systems.

Content

  • Blog
  • System Design
  • AI & ML
  • DevOps

Explore

  • About Ravi
  • Open Stats
  • Thank You

© 2026 Ravi Kant Shukla. All rights reserved.

Deployed on Vercel · Mumbai region

Back to Writing
devops

CI/CD for Spring Boot Microservices: From Commit to Production

A practical, production-minded guide to building a CI/CD pipeline for Spring Boot microservices, covering tests, quality gates, Docker image tagging, environment promotion, blue-green and canary deployments, rollbacks, and secrets.

December 1, 202619 min read
DevOpsCI/CDSpring BootMicroservicesGitHub ActionsDocker

The most dangerous deployment process is the one that depends on memory.

Someone remembers which branch to deploy. Someone remembers which Maven command to run. Someone remembers to update the Docker tag, copy the right environment variables, restart the right service, and check the right dashboard afterward.

That works until a Friday evening hotfix, a sleepy on-call engineer, a half-documented service, or a production incident turns "just deploy it" into a small adventure.

CI/CD exists to remove that drama.

For Spring Boot microservices, a good pipeline should do more than compile Java and push a Docker image. It should answer a sequence of production questions:

  • Is the code correct enough to merge?
  • Does the service still honor its API contracts?
  • Is the container image reproducible and traceable?
  • Did we scan for obvious security and dependency risks?
  • Can we promote the same artifact across environments?
  • Can we deploy gradually instead of betting everything on one release?
  • Can we roll back without inventing a procedure during an incident?

This post walks through a practical CI/CD design for a Spring Boot microservice from commit to production. The examples use GitHub Actions because it is familiar and portable, but the architecture applies equally to GitLab CI, Jenkins, Buildkite, CircleCI, Argo CD, or Spinnaker-style delivery platforms.

The goal is not a glamorous pipeline. The goal is a boring one.


What a Production Pipeline Should Optimize For

Many teams measure CI/CD by speed alone: "How fast can we deploy after a commit?"

Speed matters, but speed without guardrails is just a faster way to ship defects.

For backend microservices, a healthy pipeline optimizes for four things:

GoalWhat it means
ConfidenceTests, scans, and reviews catch common failures before production
TraceabilityEvery running service can be linked to a commit, image digest, and release
RepeatabilityThe same process runs every time, without manual shell history
RecoverabilityBad releases can be rolled back or stopped before full blast radius

The pipeline should make the right path easy:

The shape is simple:

  1. Build once.
  2. Test before packaging.
  3. Scan before publishing.
  4. Publish an immutable artifact.
  5. Promote that artifact through environments.
  6. Deploy progressively.
  7. Watch the release after it goes live.

The important phrase is build once. Do not rebuild different artifacts for dev, staging, and production. If staging tested image orders-api:sha-8f4c2a1, production should run the same image digest with different configuration.


The Example Service

Imagine an orders-api Spring Boot service:

orders-api/
  src/main/java/com/example/orders/
  src/test/java/com/example/orders/
  pom.xml
  Dockerfile
  .github/workflows/ci.yml

It exposes REST APIs for order creation and lookup. It talks to PostgreSQL, publishes events to Kafka, and calls a payment service. That makes it a realistic microservice because deployments can fail in more than one way:

  • Application code can break.
  • Database migrations can be incompatible.
  • API contracts can change.
  • Docker images can be tagged incorrectly.
  • Secrets can leak.
  • Kafka event formats can drift.
  • Kubernetes health checks can be wrong.
  • A new version can pass tests but fail under production traffic.

CI/CD is the control system around all of that.


Branching Strategy: Prefer Trunk-Based Development

Before pipeline YAML, decide how code moves.

Two common strategies dominate backend teams:

StrategyHow it worksTrade-off
GitFlowLong-lived branches like develop, release/*, and mainUseful for scheduled releases, but slow and merge-heavy
Trunk-based developmentSmall pull requests merge frequently into mainFaster delivery, requires strong tests and feature flags

For microservices, I prefer trunk-based development unless the organization has a strong reason not to.

Long-lived branches create integration pain. The code "works on my branch" for days, then fails when several changes collide. In microservices, this is worse because one service branch may depend on another service branch, a schema branch, and an infrastructure branch.

A trunk-based workflow looks like this:

feature branch -> pull request -> CI checks -> merge to main -> deploy to dev -> promote

Keep pull requests small. Hide unfinished behavior behind feature flags. Use backward-compatible database migrations. Let main stay deployable.

Feature flags are not a substitute for testing, but they separate deployment from release:

  • Deployment: new code is running in production.
  • Release: users can access the new behavior.

That separation is one of the biggest unlocks in reliable delivery.


Pipeline Stages

A production pipeline for a Spring Boot service usually has these stages:

StagePurpose
CompileCatch syntax, dependency, and annotation processing errors
Unit testsVerify isolated business logic quickly
Integration testsVerify database, messaging, and external boundary behavior
Contract testsProtect API compatibility between services
Static analysisCatch code quality and maintainability issues
Dependency scanFind known vulnerable libraries
Container buildPackage the service into a deployable image
Image scanCheck the final runtime artifact
PublishPush immutable image to registry
DeployUpdate runtime environment
Smoke testVerify the deployed service answers basic requests
ObserveWatch metrics, logs, traces, and release health

Not every service needs every gate on day one, but skipping all gates because "we are moving fast" creates a debt that compounds.

The trick is to separate fast checks from slower checks.

Run fast checks on every pull request:

  • Compile
  • Unit tests
  • Formatting or linting
  • Static analysis
  • Dependency checks

Run heavier checks on merge to main or before promotion:

  • Testcontainers integration tests
  • Contract tests across services
  • Full image scans
  • Staging smoke tests
  • Load or performance smoke tests

CI should protect developer flow without turning every pull request into a 45-minute queue.


Maven Build and Test Baseline

A Spring Boot service should have a predictable build command.

For Maven:

Shell
./mvnw -B clean verify

For Gradle:

Shell
./gradlew clean check

Use the wrapper (mvnw or gradlew) in CI. That pins the build tool version and avoids "works on my laptop because my Maven is different."

A useful Maven setup separates unit and integration tests:

pom.xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.5</version>
      <configuration>
        <includes>
          <include>**/*Test.java</include>
        </includes>
      </configuration>
    </plugin>

    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>3.2.5</version>
      <configuration>
        <includes>
          <include>**/*IT.java</include>
        </includes>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>integration-test</goal>
            <goal>verify</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Then name tests intentionally:

OrderPricingServiceTest.java       # fast unit test
CreateOrderRepositoryIT.java       # database integration test
PaymentClientContractTest.java     # API contract test

This naming discipline lets the pipeline run different gates at different moments.


Integration Tests with Testcontainers

Mocking a repository is useful for unit tests. It is not enough for a microservice pipeline.

If your service depends on PostgreSQL, Redis, Kafka, or a message broker, the pipeline needs at least a thin integration layer that exercises those boundaries.

Testcontainers is a strong default for Spring Boot:

OrderRepositoryIT.java
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("orders")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private OrderRepository repository;

    @Test
    void savesAndFindsOrderById() {
        Order order = repository.save(new Order(UUID.randomUUID(), "CREATED"));

        Optional<Order> result = repository.findById(order.getId());

        assertThat(result).isPresent();
        assertThat(result.get().getStatus()).isEqualTo("CREATED");
    }
}

This catches problems that mocks cannot:

  • Wrong column names
  • Broken Flyway or Liquibase migrations
  • JSON serialization mismatch
  • Incorrect transaction boundaries
  • PostgreSQL-specific SQL issues

For Kafka, you can use a containerized broker and verify that events are published with the expected key, topic, and payload shape.

The goal is not to recreate production inside CI. The goal is to test real integration points before they surprise you during deployment.


Contract Tests: Stop Breaking Other Services

Microservices fail at the boundaries.

If orders-api changes POST /orders and checkout-api still sends the old payload, both teams may pass their own unit tests while the product breaks.

Consumer-driven contract testing gives you a safety net. The consumer defines the interaction it relies on. The provider verifies it still honors that interaction.

A simplified Pact-style contract might say:

create-order-contract.json
{
  "consumer": "checkout-api",
  "provider": "orders-api",
  "request": {
    "method": "POST",
    "path": "/orders",
    "body": {
      "customerId": "c-123",
      "items": [{ "sku": "book-1", "quantity": 2 }]
    }
  },
  "response": {
    "status": 201,
    "body": {
      "orderId": "uuid",
      "status": "CREATED"
    }
  }
}

In a mature setup:

  • Pull requests run provider contract verification.
  • The provider publishes contract verification results.
  • Deployment is blocked if an active consumer would break.

Contract tests are especially useful when teams deploy independently. They let you move quickly without coordinating every release in a meeting.


Static Analysis and Security Gates

CI/CD should catch low-effort mistakes automatically.

For a Java service, add:

  • Checkstyle or Spotless for formatting
  • Error Prone, PMD, or SpotBugs for code smell detection
  • OWASP Dependency-Check, Snyk, Dependabot, or GitHub dependency review for vulnerable libraries
  • Secret scanning for accidentally committed tokens
  • Container image scanning with Trivy, Grype, or registry-native scanners

Do not make every warning fail the build on day one. That creates alert fatigue. Start with high-confidence gates:

  • Fail on test failures.
  • Fail on critical dependency vulnerabilities with available fixes.
  • Fail on detected secrets.
  • Fail on container images with critical CVEs in runtime packages.
  • Report lower severity issues as annotations or scheduled reports.

Quality gates should be strict enough to matter and calm enough that engineers do not learn to ignore them.


Docker Image Design for Spring Boot

A CI/CD pipeline ships an artifact. For microservices, that artifact is usually a Docker image.

A practical Spring Boot Dockerfile:

Dockerfile
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

RUN addgroup -S spring && adduser -S spring -G spring

COPY target/orders-api.jar app.jar

USER spring

EXPOSE 8080

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

For larger services, use Spring Boot layered jars so Docker cache reuse is better:

Shell
java -Djarmode=layertools -jar target/orders-api.jar extract

Then copy dependency layers separately. This reduces image rebuild time when only application classes change.

The container image should not contain environment-specific configuration. Put runtime configuration in environment variables, Kubernetes Secrets, ConfigMaps, or your platform's secret manager.

Bad:

Dockerfile
ENV SPRING_PROFILES_ACTIVE=production
ENV DATABASE_PASSWORD=super-secret

Better:

Yaml
env:
  - name: SPRING_PROFILES_ACTIVE
    value: production
  - name: DATABASE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: orders-api-secrets
        key: database-password

The image should be portable. The environment decides how it runs.


Image Tagging: Never Deploy Ambiguous Images

The fastest way to make rollback painful is to deploy latest.

latest is a moving pointer. It tells you almost nothing during an incident. Which commit is running? When was it built? Did staging test the same artifact?

Use immutable tags and image digests.

A good tagging strategy:

orders-api:sha-8f4c2a1
orders-api:build-2026-12-01-1042
orders-api:v1.18.0

The deployment should ideally pin the digest:

registry.example.com/orders-api@sha256:3a4f...

Tags are human-friendly. Digests are immutable truth.

Add OCI labels to the image:

Dockerfile
LABEL org.opencontainers.image.source="https://github.com/example/orders-api"
LABEL org.opencontainers.image.revision="$GIT_SHA"
LABEL org.opencontainers.image.created="$BUILD_TIME"

During an incident, you should be able to answer:

  • What version is running?
  • Which commit produced it?
  • Which pipeline run built it?
  • Who approved production promotion?
  • Which previous version can we roll back to?

If the pipeline cannot answer those questions, it is not production-ready yet.


A GitHub Actions Workflow

Here is a complete but still readable GitHub Actions pipeline.

It runs tests on pull requests. On merge to main, it builds and publishes the Docker image. Deployment jobs are shown as placeholders because the final command depends on your platform: Kubernetes, ECS, Nomad, Cloud Run, or another runtime.

.github/workflows/ci.yml
name: orders-api-ci

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: example/orders-api
  JAVA_VERSION: "21"

jobs:
  test:
    name: Build and test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ env.JAVA_VERSION }}
          cache: maven

      - name: Run tests
        run: ./mvnw -B clean verify

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: |
            target/surefire-reports
            target/failsafe-reports

  dependency-scan:
    name: Dependency scan
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Dependency review
        if: github.event_name == 'pull_request'
        uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: critical

  package:
    name: Build and publish image
    needs: [test, dependency-scan]
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    outputs:
      image: ${{ steps.meta.outputs.tags }}
      digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ env.JAVA_VERSION }}
          cache: maven

      - name: Build jar
        run: ./mvnw -B -DskipTests package

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=raw,value=main

      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  deploy-dev:
    name: Deploy to dev
    needs: package
    runs-on: ubuntu-latest
    environment: dev

    steps:
      - name: Deploy image
        run: |
          echo "Deploy ${{ needs.package.outputs.digest }} to dev"

  promote-staging:
    name: Promote to staging
    needs: deploy-dev
    runs-on: ubuntu-latest
    environment: staging

    steps:
      - name: Promote image
        run: |
          echo "Promote tested image to staging"

  promote-production:
    name: Promote to production
    needs: promote-staging
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Promote image
        run: |
          echo "Promote tested image to production"

This is intentionally platform-neutral. In a Kubernetes setup, the deploy step might update a Helm release. In ECS, it might register a new task definition. In GitOps, it might open a pull request against an environment repository.

The key behavior remains the same: the image is built once and promoted.


Environment Promotion

A common anti-pattern is rebuilding per environment:

Build dev image -> deploy dev
Build staging image -> deploy staging
Build production image -> deploy production

That means production runs something staging never tested.

Use artifact promotion instead:

Build image sha-8f4c2a1
Deploy sha-8f4c2a1 to dev
Promote sha-8f4c2a1 to staging
Promote sha-8f4c2a1 to production

Only configuration should differ:

EnvironmentDifference
DevSmaller database, debug-friendly settings, fake dependencies where acceptable
StagingProduction-like topology, realistic integration, limited access
ProductionReal users, strict secrets, strict observability, rollback plan

Environment promotion is not ceremony. It is how you create confidence before exposing real users.


Database Migrations Without Downtime

Most Spring Boot deployment incidents are not caused by Java syntax errors. They are caused by data shape changes.

If you deploy code and schema as a single fragile switch, you will eventually break production.

Use backward-compatible migrations:

Step 1: Add new nullable column.
Step 2: Deploy code that writes both old and new columns.
Step 3: Backfill existing rows.
Step 4: Deploy code that reads the new column.
Step 5: Stop writing the old column.
Step 6: Drop the old column in a later release.

For example:

V42__add_order_status_reason.sql
ALTER TABLE orders
ADD COLUMN status_reason TEXT;

This is safe because old code ignores the new column and new code can handle it being null.

Be careful with:

  • Dropping columns
  • Renaming columns
  • Adding non-null columns without defaults
  • Long-running table locks
  • Backfills inside application startup
  • Migrations that assume low traffic

Do not let every service run Flyway migrations automatically if several replicas start at the same time. Prefer a single migration job before application rollout, or ensure your migration tool uses locking correctly.

Deployment reliability is often database migration discipline wearing a DevOps hat.


Blue-Green Deployments

In a blue-green deployment, two production environments exist:

  • Blue: currently serving users
  • Green: new version, warmed up and tested

Traffic switches from blue to green only after green passes health checks and smoke tests.

Blue-green is excellent when:

  • You need fast rollback.
  • You can afford duplicate capacity during deploy.
  • Your app is stateless.
  • Database migrations are backward compatible.

Rollback is simple: route traffic back to blue.

The hard part is shared state. If green writes data in a format blue cannot read, rollback is no longer safe. That is why deployment strategy and migration strategy must be designed together.


Canary Deployments

Canary deployment sends a small percentage of production traffic to the new version first.

Example:

5% traffic -> v2 for 10 minutes
25% traffic -> v2 for 20 minutes
50% traffic -> v2 for 20 minutes
100% traffic -> v2

At every step, watch release health:

  • Error rate
  • p95 and p99 latency
  • JVM memory and GC
  • Database connection pool saturation
  • Kafka publish failures
  • Payment provider failures
  • Business metrics such as checkout success rate

Canary works best when the routing layer can split traffic by weight, header, user segment, or region. Kubernetes users often implement this with Argo Rollouts, Flagger, Istio, Linkerd, Nginx, or a cloud load balancer. ECS and other platforms can do similar progressive traffic shifting through their deployment controllers or load balancer integrations.

The key is automated judgment. A canary is weak if it only waits for time to pass.

Better:

Advance rollout only if:
- 5xx rate is below 1%
- p95 latency is below 400ms
- no critical alerts are firing
- checkout success rate has not dropped

Progressive delivery is not just traffic splitting. It is traffic splitting plus production feedback.


Rollback Strategies

Rollback should be boring.

Before every production release, the pipeline should know:

  • The currently running version
  • The candidate version
  • The previous known-good version
  • Whether database migrations are rollback-safe
  • Which dashboards and alerts define release health

There are three common rollback styles:

StrategyHow it worksRisk
Revert trafficBlue-green or canary sends users back to old versionFast, but requires state compatibility
Redeploy previous imageRuntime is updated to previous image digestWorks when deploy platform supports quick replacement
Code revertNew commit reverts the change and runs full pipelineSafest for complex changes, slower

Do not assume rollback always means "go back to the old code." If the new version has already changed data, old code may not understand it.

For risky releases, create a forward-fix plan too:

  • Disable feature flag.
  • Stop a consumer.
  • Pause a scheduled job.
  • Increase timeout or circuit breaker threshold.
  • Roll forward with a hotfix.

Rollback is one recovery tool, not a magic spell.


Secrets in CI/CD

Secrets do not belong in code, Docker images, logs, or pull request comments.

CI/CD systems should use scoped secret storage:

  • GitHub Actions secrets or environment secrets
  • AWS Secrets Manager
  • AWS Systems Manager Parameter Store
  • HashiCorp Vault
  • GCP Secret Manager
  • Azure Key Vault
  • Kubernetes Secrets, preferably encrypted and managed carefully

Use different credentials per environment. The CI job that deploys to dev should not have production database credentials.

Prefer short-lived credentials over static tokens. In GitHub Actions, OpenID Connect can let the workflow assume a cloud role without storing long-lived cloud keys:

.github/workflows/deploy.yml
permissions:
  id-token: write
  contents: read

steps:
  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/orders-api-deployer
      aws-region: ap-south-1

Good secret hygiene also means:

  • Mask secrets in logs.
  • Rotate credentials regularly.
  • Use least-privilege deployment roles.
  • Separate build permissions from deploy permissions.
  • Require manual approval for production environments when appropriate.

CI/CD is a privileged path into production. Treat it like one.


Smoke Tests After Deployment

Deployment is not done when the command succeeds.

A smoke test verifies that the service is actually alive in the target environment:

Shell
curl --fail --silent https://api.example.com/orders/health/ready

For orders-api, useful smoke checks might include:

  • /actuator/health/readiness returns healthy.
  • The service can reach PostgreSQL.
  • The service can publish to Kafka.
  • A read-only endpoint returns expected shape.
  • Authentication middleware accepts a valid test token.
  • No new critical logs appear in the first few minutes.

Spring Boot Actuator gives you a strong baseline:

application.yml
management:
  endpoint:
    health:
      probes:
        enabled: true
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

Expose only what is appropriate. Health endpoints are useful; accidentally exposing sensitive actuator endpoints is not.


Observability as a Release Gate

The pipeline should not throw code over the wall and hope monitoring catches it later.

Release health should be visible during rollout:

Version: orders-api sha-8f4c2a1
Traffic: 25%
5xx rate: 0.2%
p95 latency: 180ms
p99 latency: 620ms
DB pool usage: 48%
Kafka publish failures: 0
Rollback target: sha-2a91b72

Every service should emit version metadata:

application.yml
management:
  info:
    git:
      mode: full

And logs should include trace IDs, request IDs, and version labels where possible. When an incident starts after deployment, you need to quickly separate:

  • Is the new version failing?
  • Is only one pod failing?
  • Is one dependency failing?
  • Is one region affected?
  • Is this a traffic pattern change unrelated to the deployment?

CI/CD and observability are not separate disciplines. A deployment pipeline without observability is a car without a dashboard.


GitOps Option: CI Builds, CD Reconciles

Many teams eventually split CI and CD:

  • CI builds and tests the artifact.
  • CD watches desired state and applies it to the cluster.

In a GitOps model with Argo CD or Flux, the CI pipeline does not directly run kubectl apply against production. Instead, it updates an environment repository:

environments/prod/orders-api/values.yaml
image:
  repository: ghcr.io/example/orders-api
  tag: sha-8f4c2a1

Argo CD notices the change and reconciles the cluster to match Git.

This creates a clean audit trail:

  • Pull request changed production desired state.
  • Review approved it.
  • Argo applied it.
  • Cluster converged or failed.

GitOps is not required for every team. But it is a strong pattern when you have many services, multiple environments, and a platform team that wants consistent deployment behavior.


Common CI/CD Mistakes

Deploying Unversioned Images

latest and mutable tags make incidents harder. Use immutable tags and record image digests.

Rebuilding for Production

If production builds a fresh artifact, staging did not test what production runs. Build once, promote the same artifact.

Skipping Integration Tests

Unit tests are valuable, but most service failures happen at boundaries: database, Kafka, HTTP clients, serialization, and configuration.

Running Migrations Casually

Schema changes need backward compatibility. Treat migrations as part of release design, not an afterthought.

Putting Secrets in Images

Images travel through registries, caches, scanners, and developer machines. Keep secrets in runtime secret stores.

No Rollback Practice

A rollback procedure that has never been tested is a wish. Practice rollback in staging and after low-risk production deploys.

Release Freeze as the Only Safety Mechanism

Freezes reduce change, but they do not improve the deployment system. Strong pipelines, small changes, and progressive delivery reduce risk more sustainably.


A Practical Rollout Checklist

Before merging:

  • Unit tests pass.
  • Integration tests pass for database and messaging boundaries.
  • Contract tests pass for active consumers.
  • Static analysis has no critical failures.
  • Dependency scan has no critical unresolved issues.

Before staging:

  • Docker image is tagged by commit SHA.
  • Image scan passes high-confidence gates.
  • Database migrations are reviewed for compatibility.
  • Smoke tests are defined.

Before production:

  • Same image tested in staging is selected.
  • Rollback target is known.
  • Feature flags are configured.
  • Dashboards are open or linked from the pipeline.
  • Production approval is recorded if required.

After production:

  • Smoke tests pass.
  • Error rate and latency remain healthy.
  • Logs show no new repeated failures.
  • Business metric does not drop unexpectedly.
  • Release notes or deployment history are updated.

This checklist should live close to the pipeline, not in an abandoned document.


Final Takeaway

CI/CD for Spring Boot microservices is not just YAML.

It is a delivery system made of branching strategy, test design, contract discipline, image immutability, environment promotion, deployment strategy, rollback planning, secret management, and observability.

The mature version is simple to describe:

Build once. Test hard. Scan the artifact. Tag it immutably. Promote it through environments. Deploy gradually. Watch production. Roll back quickly when needed.

That is how teams ship faster without making production fragile.

In the next DevOps post, we will move from pipeline design into runtime reality: Kubernetes for Backend Engineers, covering pods, deployments, probes, resource limits, JVM behavior, graceful shutdown, and the production pitfalls that show up after the container starts.


Subscribe to get DevOps, system design, and backend engineering posts in your inbox every two weeks.

R

Ravi Kant Shukla

Senior Java + AI engineer. 9+ years in system design, Kafka, microservices, and LLM/RAG pipelines.

About Ravi →More Posts →

Enjoyed this post?

Get more system design and AWS insights delivered weekly. No spam.

Comments (0)

Loading comments...

Leave a comment

Your email will not be displayed publicly.

Related Posts

devops

CI/CD for Spring Boot Microservices: From Commit to Production

Master the journey from code commit to production deployment. Learn pipeline design, quality gates, blue-green deployments, and secrets management for Spring Boot services.

CI/CDGitHub-ActionsDevOps
May 25, 202619