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 URLsoptional:config import: flexible for dev, strict for production