Zero Restarts: Implementing a Git-Backed Config Server


Introduction

With 10+ microservices, each needing database URLs, Eureka endpoints, JWT secrets, and Kafka bootstrap servers — and all of those varying per environment — hardcoding properties doesn’t scale. PFMS uses Spring Cloud Config Server to centralize everything in a Git repository. Change a property, push to Git, and services pick it up without restarting.

The Config Server Bootstrap

The entire Config Server is a single Spring Boot application with two annotations:

// config-server/.../ConfigServerApplication.java
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

@EnableConfigServer turns this into a config endpoint. @EnableDiscoveryClient registers it with Eureka so other services can find it by name rather than hardcoded URL.

The server configuration points at the Git repository:

# config-server/src/main/resources/application.yml
server:
  port: 8888

spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/delose/personal-finance-management-system.git
          search-paths: config-repo
          clone-on-start: true
          default-label: master

clone-on-start: true means the server clones the repo during startup, not on first request. This trades a slower cold start for guaranteed availability — no first-request latency spike.

Inside the config-repo

The config-repo/ directory in the Git repository holds per-service property files. Spring Cloud Config matches files to services by spring.application.name:

config-repo/
├── api-gateway.properties          # Base config
├── api-gateway-dev.properties      # Dev overrides
├── api-gateway-docker.properties   # Docker overrides
├── budget-service.yml
└── discovery-server.yml

API Gateway Base Config

The base api-gateway.properties defines Gateway routes using Eureka’s lb:// prefix for load-balanced routing:

# config-repo/api-gateway.properties
spring.cloud.gateway.routes[0].id=user-service
spring.cloud.gateway.routes[0].uri=lb://user-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/users/**

spring.cloud.gateway.routes[1].id=budget-service
spring.cloud.gateway.routes[1].uri=lb://budget-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/v1/api/budgets/**

The lb://budget-service URI tells the Gateway to look up budget-service in Eureka and load-balance across instances. No hardcoded host or port.

Discovery Server Config

# config-repo/discovery-server.yml
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    wait-time-in-ms-when-sync-empty: 0

Eureka itself sets register-with-eureka: false — the registry doesn’t register with itself.

Multi-Profile Strategy

The real power is environment-specific overrides. The same API Gateway gets different config depending on its active Spring profile:

Dev profile (api-gateway-dev.properties):

# Local development
spring.datasource.url=jdbc:mysql://localhost:3307/apigwdb?serverTimezone=UTC
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka
eureka.client.register-with-eureka=false

Docker profile (api-gateway-docker.properties):

# Running in Docker
spring.datasource.url=jdbc:mysql://host.docker.internal:3307/apigwdb?serverTimezone=UTC
eureka.client.serviceUrl.defaultZone=http://host.docker.internal:8761/eureka
eureka.client.register-with-eureka=true

The key difference: localhost vs host.docker.internal. In Docker, containers can’t reach the host’s localhost. The Docker profile also enables Eureka registration (register-with-eureka=true) since in Docker you want services to discover each other, while in local dev you might run services individually.

A service activates a profile by setting spring.profiles.active=docker — the Config Server automatically merges the base config with the profile-specific overrides.

Fail-Fast: Why Services Crash Without Config

Services in PFMS import the Config Server with optional:configserver::

# In each service's application.properties
spring.config.import=optional:configserver:http://localhost:8888

The optional: prefix means the service will start even if the Config Server is unreachable — it falls back to its local properties. This is a pragmatic choice: during local development, you often don’t want to run the Config Server just to test one service. In production, you’d remove the optional: prefix to enforce strict dependency — no config server, no startup.

Summary

  • Centralized Git-backed config: one source of truth, version-controlled, auditable
  • clone-on-start: slower startup but no first-request delay
  • Multi-profile overrides: same service, different config per environment (dev, docker, sit)
  • lb:// routes: Gateway discovers services through Eureka, no hardcoded URLs
  • optional: config import: flexible for dev, strict for production