Spring Boot - Building Microservices in Java from Scratch to Production
Spring Boot Building
backendSpring Boot: Building Microservices in Java from Scratch to Production
Spring Boot has revolutionized the way Java applications are built. By eliminating tedious XML configuration and providing production-ready components out of the box, it allows developers to focus on business logic instead of infrastructure. In this article, we will walk you through the entire Spring Boot ecosystem — from your first REST endpoint, through security and data access, all the way to deploying microservices on Kubernetes.
What is Spring Boot and the Spring Ecosystem?#
Spring Framework is one of the most popular frameworks for building enterprise Java applications. It was created as an alternative to the heavyweight EJB model and is built on two pillars: Inversion of Control (IoC) and Dependency Injection (DI).
Spring Boot is a layer on top of Spring Framework that introduces:
- Auto-configuration — automatic detection and configuration of components
- Embedded servers — Tomcat, Jetty, or Undertow built into your application
- Spring Boot Starters — curated dependency sets for common scenarios
- Spring Boot Actuator — application monitoring and management
- Spring Boot DevTools — automatic restart and hot-reload in development mode
The Spring ecosystem encompasses dozens of projects, but the most important ones are:
- Spring MVC — REST controllers and HTTP handling
- Spring Data — simplified database access
- Spring Security — authentication and authorization
- Spring Cloud — tools for building microservices
- Spring Batch — batch processing
Auto-Configuration and Convention over Configuration#
One of the key mechanisms in Spring Boot is auto-configuration. Simply add the appropriate starter to your pom.xml or build.gradle, and Spring Boot will automatically configure all the necessary beans.
// This is everything you need to run a Spring Boot application
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
The @SpringBootApplication annotation combines three mechanisms:
@Configuration— marks the class as a source of bean definitions@EnableAutoConfiguration— enables auto-configuration@ComponentScan— scans packages for components
Application configuration is done through the application.yml file:
server:
port: 8080
spring:
application:
name: order-service
datasource:
url: jdbc:postgresql://localhost:5432/orders
username: ${DB_USERNAME:admin}
password: ${DB_PASSWORD:secret}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 20
Spring Data JPA — Database Access#
Spring Data JPA eliminates the need to write boilerplate code for CRUD operations. Just define a repository interface, and Spring will generate the implementation automatically.
JPA Entity#
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String customerEmail;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Column(nullable = false)
private BigDecimal totalAmount;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// constructors, getters, setters
}
Repository#
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerEmailOrderByCreatedAtDesc(String email);
@Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since")
Page<Order> findRecentByStatus(
@Param("status") OrderStatus status,
@Param("since") LocalDateTime since,
Pageable pageable
);
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
@Query("""
SELECT new com.example.dto.OrderSummary(
o.status, COUNT(o), SUM(o.totalAmount)
)
FROM Order o
WHERE o.createdAt BETWEEN :from AND :to
GROUP BY o.status
""")
List<OrderSummary> getOrderSummary(
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to
);
}
Spring Data automatically generates SQL queries based on method names (findByCustomerEmailOrderByCreatedAtDesc), and for more complex cases, allows defining JPQL queries using the @Query annotation.
REST Controllers and API Design#
Spring MVC provides an elegant model for creating REST controllers. It is important to follow good API design practices: appropriate HTTP status codes, input validation, and consistent error responses.
@RestController
@RequestMapping("/api/v1/orders")
@Validated
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping
public ResponseEntity<Page<OrderResponse>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) OrderStatus status) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<OrderResponse> orders = orderService.getOrders(status, pageable);
return ResponseEntity.ok(orders);
}
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrderById(id));
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderResponse created = orderService.createOrder(request);
URI location = URI.create("/api/v1/orders/" + created.id());
return ResponseEntity.created(location).body(created);
}
@PatchMapping("/{id}/status")
public ResponseEntity<OrderResponse> updateStatus(
@PathVariable Long id,
@Valid @RequestBody UpdateStatusRequest request) {
return ResponseEntity.ok(orderService.updateStatus(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void cancelOrder(@PathVariable Long id) {
orderService.cancelOrder(id);
}
}
Global Exception Handling#
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(a, b) -> a
));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
LocalDateTime.now(),
fieldErrors
);
return ResponseEntity.badRequest().body(error);
}
}
Spring Security — JWT and OAuth2#
Spring Security is a comprehensive security framework. Modern microservice applications typically use JWT tokens or the OAuth2 protocol.
Security Configuration with JWT#
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT Filter#
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Spring Cloud — Microservice Infrastructure#
Spring Cloud provides a set of tools essential for building distributed microservice systems. The key components are Service Discovery, Config Server, and API Gateway.
Eureka — Service Discovery#
Eureka allows microservices to register and discover each other without hardcoding addresses.
Eureka Server:
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
# Eureka Server application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
Client (microservice):
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
Spring Cloud Config Server#
A centralized configuration server allows managing the configuration of all microservices from a single place (e.g., a Git repository):
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
spring:
cloud:
config:
server:
git:
uri: https://github.com/my-org/config-repo
default-label: main
search-paths: '{application}'
Spring Cloud Gateway#
The Gateway serves as the central entry point to the microservice system, handling routing, load balancing, and request filtering:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/v1/orders/**
filters:
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/orders
- name: RequestRateLimiter
args:
redis-rate-limiter:
replenishRate: 10
burstCapacity: 20
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/v1/products/**
Spring Boot Actuator — Monitoring and Management#
Actuator exposes a number of endpoints for monitoring application health, which is essential in production environments.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
probes:
enabled: true
metrics:
tags:
application: ${spring.application.name}
Custom Health Indicator#
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
public ExternalApiHealthIndicator(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public Health health() {
try {
ResponseEntity<String> response =
restTemplate.getForEntity("https://api.external-service.com/health", String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("externalApi", "Available")
.withDetail("responseTime", "OK")
.build();
}
return Health.down().withDetail("externalApi", "Unhealthy").build();
} catch (Exception e) {
return Health.down()
.withDetail("externalApi", "Unavailable")
.withException(e)
.build();
}
}
}
Spring Profiles — Environment Management#
Profiles allow you to define different configurations for different environments: development, staging, and production.
# application.yml — common configuration
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example: DEBUG
org.hibernate.SQL: DEBUG
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
logging:
level:
com.example: WARN
Profile activation is done via an environment variable or a startup parameter:
java -jar app.jar --spring.profiles.active=prod
# or
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
Testing — MockMvc and Testcontainers#
Spring Boot offers a rich testing ecosystem. Integration tests with MockMvc allow you to test controllers without starting an HTTP server, while Testcontainers enable testing with a real database running in Docker.
Controller Tests with MockMvc#
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldCreateOrder() throws Exception {
CreateOrderRequest request = new CreateOrderRequest(
"customer@example.com",
List.of(new OrderItemRequest(1L, 2))
);
OrderResponse response = new OrderResponse(
1L, "customer@example.com", OrderStatus.CREATED,
new BigDecimal("199.98"), LocalDateTime.now()
);
when(orderService.createOrder(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.status").value("CREATED"))
.andExpect(header().exists("Location"));
}
@Test
void shouldReturn400ForInvalidRequest() throws Exception {
CreateOrderRequest request = new CreateOrderRequest("", List.of());
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.fieldErrors").exists());
}
}
Integration Tests with Testcontainers#
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(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 TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
}
@Test
void shouldCreateAndRetrieveOrder() {
CreateOrderRequest request = new CreateOrderRequest(
"test@example.com",
List.of(new OrderItemRequest(1L, 3))
);
ResponseEntity<OrderResponse> createResponse = restTemplate
.postForEntity("/api/v1/orders", request, OrderResponse.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
Long orderId = createResponse.getBody().id();
ResponseEntity<OrderResponse> getResponse = restTemplate
.getForEntity("/api/v1/orders/" + orderId, OrderResponse.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().customerEmail()).isEqualTo("test@example.com");
}
}
Docker and Kubernetes — Production Deployment#
Dockerfile with Multi-Stage Build#
# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
Kubernetes Deployment#
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Performance Tuning#
Spring Boot application performance depends on many factors. Here are the most important areas to optimize.
Connection Pools with HikariCP#
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
max-lifetime: 1200000
leak-detection-threshold: 60000
Caching with Spring Cache#
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
return productRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
}
@CacheEvict(value = "products", key = "#id")
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
// update product
}
@CacheEvict(value = "products", allEntries = true)
@Scheduled(fixedRate = 3600000)
public void evictAllProductsCache() {
// automatic cache eviction every hour
}
}
JPA Query Optimization#
// N+1 problem — solution with EntityGraph
@EntityGraph(attributePaths = {"items", "items.product"})
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findOrdersWithItems(@Param("email") String email);
// Projections — fetching only needed columns
public interface OrderSummaryProjection {
Long getId();
String getCustomerEmail();
OrderStatus getStatus();
BigDecimal getTotalAmount();
}
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<OrderSummaryProjection> findSummariesByStatus(@Param("status") OrderStatus status);
When to Choose Spring Boot?#
Spring Boot is an excellent choice when:
- You are building enterprise applications — a mature ecosystem with thousands of libraries
- You need microservices — Spring Cloud provides full support
- Your team knows Java — the Spring Boot learning curve is gentle
- Performance matters — JVM offers excellent performance for long-running processes
- You need enterprise-grade security — Spring Security is the industry standard
- Integration with legacy systems — Java has the widest range of connectors
Consider alternatives (Quarkus, Micronaut) if startup time or memory consumption in serverless environments is a priority.
Summary#
Spring Boot is a powerful platform that covers virtually every aspect of building modern applications:
- Auto-configuration drastically reduces boilerplate
- Spring Data JPA simplifies data access
- Spring Security provides enterprise-level security
- Spring Cloud delivers microservice infrastructure
- Actuator enables production monitoring
- Testcontainers allow realistic integration testing
The key to success is knowing the ecosystem and applying best practices from the very beginning of your project.
Need Help with Java Projects?#
At MDS Software Solutions Group, we specialize in building enterprise applications using Spring Boot and microservices. We offer:
- Microservice architecture design
- Spring Boot application development from scratch
- Monolith-to-microservice migration
- Docker and Kubernetes deployments
- Performance and security audits
- Technical support and training
Contact us to discuss your project!
Team of programming experts specializing in modern web technologies.