Przejdź do treści
Backend

Spring Boot - Building Microservices in Java from Scratch to Production

Published on:
·5 min read·Author: MDS Software Solutions Group

Spring Boot Building

backend

Spring 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!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Spring Boot - Building Microservices in Java from Scratch to Production | MDS Software Solutions Group | MDS Software Solutions Group