Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Configuring Spring Boot with application.yml

Recommended resources

Hello! Welcome back to our course on building a production-grade rate limiter.

In our last lesson, you gained fine-grained control over API responses using ResponseEntity. We modified the GreetingController to return a 400 Bad Request for invalid input, but in doing so, we left a piece of text hardcoded: private static final String template = "Hello, %s!";. While this works, hardcoding values that might need to change is not a scalable practice. What if you wanted to change the greeting message without recompiling the entire application?

This lesson addresses exactly that. We'll dive into one of Spring Boot's most powerful features: externalized configuration. You will learn how to move application settings out of your Java code and into a simple, human-readable file called application.yml. This skill is fundamental for building the flexible and maintainable rate limiter we're aiming for.

Your learning outcome for this lesson:

Use Spring Boot's application.yml for basic application configuration.



1. The "Why": Separating Code from Configuration

In any robust software system, it's crucial to separate the code (the logic) from the configuration (the settings that influence that logic). This principle allows you to:

  • Modify application behavior without changing the source code.
  • Use the same application artifact (the compiled .jar file) across different environments (development, testing, production) just by changing the configuration.
  • Allow non-developers (like system administrators) to tweak settings easily.

Spring Boot makes this incredibly easy by automatically looking for configuration files in specific locations. The two most common formats are .properties and .yml (YAML).

To understand the difference and why YAML is often preferred, watch the first part of this video.

application.yml vs application.properties in SpringBoot | Properties vs YAML | Geekific

The video 'application.yml vs application.properties in SpringBoot' from Geekific provides a concise introduction to external configuration and compares the two primary formats.

Please watch these sections: Introduction (00:14 - 00:44): Get a quick overview of external configuration in Spring Boot. Properties Format (00:44 - 01:18): Understand the traditional key-value format of .properties files. YAML Introduction (02:01 - 02:32): See how YAML provides a more readable, hierarchical structure.

The key takeaway is that YAML's hierarchical nature makes it much cleaner for complex, nested configurations, which is exactly what we'll need for defining our rate limiting rules later on.

Comparison of application.yaml and application.properties in Spring Boot
This image provides a side-by-side comparison of `application.yaml` and `application.properties` files. Notice how the YAML format on the left uses indentation to create a clear, nested structure, while the properties format on the right relies on repetitive dot-notation prefixes.


2. Accessing Configuration with @Value

The simplest way to read a property from application.yml is with the @Value annotation. It injects a single configuration value directly into a field in your Spring bean.

Let's put this into practice by externalizing the greeting template from our GreetingController.

Configuration with YAML - Spring Boot - TechieLearns

The article 'Configuration with YAML' by TechieLearns provides a clear, step-by-step guide. We'll use it to create our first application.yml file and access its properties.

Please read the sections 'Getting Started: Creating Your First application.yml File' and 'Accessing YAML Configuration in Your Java Code', focusing on the part about the @Value annotation. Pay close attention to the file location (src/main/resources) and the syntax (${property.key}).

Exercise 1: Refactor GreetingController to use @Value

Now, apply what you've just read to your project.

  1. Create the application.yml file: In your project's src/main/resources directory, create a new file named application.yml. If an application.properties file already exists, you can either delete it or rename it to avoid confusion.

  2. Add a custom property: Add the following content to your new application.yml file. This defines a custom property greeting.template.

    greeting:
      template: "Hello from config, %s!"
    
  3. Update GreetingController.java: Modify your controller to inject this value.

    • Remove the private static final String template = ...; line.
    • Add a new field annotated with @Value.

    Your controller should now look like this:

    package com.example.ratelimiter.controller;
    
    import com.example.ratelimiter.dto.Greeting;
    import org.springframework.beans.factory.annotation.Value; // <-- Import this
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    @RestController
    public class GreetingController {
    
        // The template is now injected from application.yml
        @Value("${greeting.template}")
        private String template;
    
        private final AtomicLong counter = new AtomicLong();
    
        @GetMapping("/greeting")
        public ResponseEntity<Object> greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
            if (name == null || name.trim().isEmpty()) {
                return ResponseEntity
                        .status(HttpStatus.BAD_REQUEST)
                        .body("The 'name' parameter cannot be empty.");
            }
    
            Greeting body = new Greeting(counter.incrementAndGet(), String.format(template, name));
            return ResponseEntity.ok(body);
        }
    }
    
  4. Run and test: Start your application and visit http://localhost:8080/greeting. The response should now be {"id":1,"content":"Hello from config, World!"}. You've successfully separated configuration from your code!



3. Structured Configuration with @ConfigurationProperties

Using @Value is great for simple, isolated properties. However, as configurations grow, scattering @Value annotations across your codebase can become messy. For our rate limiter, we'll need to define multiple related properties for each rule (e.g., limit, time window, algorithm).

A much cleaner, type-safe, and more scalable approach is to use the @ConfigurationProperties annotation. This annotation binds a whole tree of properties into a dedicated Java object.

Spring Boot Property Binding with @ConfigurationProperties
This diagram shows the core idea of `@ConfigurationProperties`: it maps a block of configuration keys and values from your `application.yml` file directly to the fields of a strongly-typed Java class.

This approach offers several advantages, which are nicely summarized in the following article.

ConfigurationProperties in Spring Boot

The article 'ConfigurationProperties in Spring Boot' from Learn Code With Durgesh provides an excellent comparison between @Value and @ConfigurationProperties and outlines the best practices for structured configuration.

Please read these key sections: Why Use @ConfigurationProperties?: Understand the main benefits. @ConfigurationProperties vs @Value: Focus on the comparison table, which clearly contrasts the two approaches. Best Practices: Absorb the advice on how to structure your configuration classes for maintainability.

To enable better IDE support (like auto-completion and validation) for your custom properties, it's a best practice to add the spring-boot-configuration-processor dependency. In your pom.xml, add this inside the <dependencies> block:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Exercise 2: Refactor to use @ConfigurationProperties

Let's refactor our simple greeting configuration to use this more robust pattern.

  1. Create a properties class: Create a new package com.example.ratelimiter.config and add a new Java class named AppProperties.java.

    package com.example.ratelimiter.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConfigurationProperties(prefix = "app")
    public class AppProperties {
    
        private String name = "Rate Limiter API"; // A default value
        private Greeting greeting = new Greeting();
    
        public static class Greeting {
            private String template = "Hello, %s!"; // Default template
            private String defaultName = "World";   // Default name
    
            // --- Getters and Setters ---
            public String getTemplate() { return template; }
            public void setTemplate(String template) { this.template = template; }
            public String getDefaultName() { return defaultName; }
            public void setDefaultName(String defaultName) { this.defaultName = defaultName; }
        }
    
        // --- Getters and Setters ---
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public Greeting getGreeting() { return greeting; }
        public void setGreeting(Greeting greeting) { this.greeting = greeting; }
    }
    

    Note: The @Component annotation makes this class a Spring bean, and @ConfigurationProperties(prefix = "app") tells Spring to bind all properties starting with app to this object.

  2. Update application.yml: Modify your YAML to match this new structure.

    app:
      name: "My Awesome Rate Limiter"
      greeting:
        template: "Welcome, %s!"
        default-name: "User"
    
  3. Update GreetingController.java: Inject the AppProperties bean and use it.

    package com.example.ratelimiter.controller;
    
    import com.example.ratelimiter.config.AppProperties; // <-- Import this
    import com.example.ratelimiter.dto.Greeting;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    @RestController
    public class GreetingController {
    
        private final AppProperties appProperties; // <-- Inject the properties bean
        private final AtomicLong counter = new AtomicLong();
    
        // Use constructor injection - a best practice
        public GreetingController(AppProperties appProperties) {
            this.appProperties = appProperties;
        }
    
        @GetMapping("/greeting")
        public ResponseEntity<Object> greeting(@RequestParam(value = "name", required = false) String name) {
            // Use the default name from config if the parameter is missing
            String effectiveName = (name != null && !name.trim().isEmpty()) ? name : appProperties.getGreeting().getDefaultName();
    
            if (effectiveName.trim().isEmpty()) {
                 return ResponseEntity
                        .status(HttpStatus.BAD_REQUEST)
                        .body("The 'name' parameter cannot be empty.");
            }
            
            String template = appProperties.getGreeting().getTemplate();
            Greeting body = new Greeting(counter.incrementAndGet(), String.format(template, effectiveName));
            return ResponseEntity.ok(body);
        }
    }
    

    Notice we also updated the @RequestParam to be required = false and handle the default name logic inside the method.

  4. Run and test again:

    • Visit http://localhost:8080/greeting. You should see {"id":1,"content":"Welcome, User!"}.
    • Visit http://localhost:8080/greeting?name=Student. You should see {"id":2,"content":"Welcome, Student!"}.

You have now successfully implemented a structured, type-safe configuration system. This pattern will be the foundation for defining our complex rate-limiting rules in future lessons.



Key Takeaways

  • Externalized configuration separates your application's logic from its settings, making it more flexible and easier to manage across different environments.
  • YAML (.yml) is the preferred format for Spring Boot configuration due to its readability and support for hierarchical data.
  • The @Value annotation is suitable for injecting single, simple configuration properties.
  • The @ConfigurationProperties annotation is the best practice for binding groups of related properties to a type-safe Java object, which is ideal for complex configurations like our future rate limiter rules.

Next Steps

We now have a configurable API endpoint. The next logical step in building our rate limiter is to find a way to intercept incoming requests before they even reach the GreetingController. This "interception" point is where our rate-limiting logic will live.

In the next lesson, we will learn how to implement request interceptors using HandlerInterceptor for cross-cutting concerns. This will provide the architectural hook we need to plug our rate limiter into the Spring request lifecycle.