Edupoly · Spring Foundation

Stage 12: Create beans with explicit factory methods

Start simply: what are we learning?

Stage 12 teaches another way to give Spring an object to manage. Earlier we marked our classes with @Component, @Service or @Controller. Here we also write @Bean methods that return objects for Spring to manage. The returned object is the bean; the method producing it is the factory method.

What is the business meaning of catalog?

Catalog means our collection of products. It is a business name we chose, not a Spring keyword. Imagine a shop displaying prices in INR and offering a well-stocked-products page that includes only products with at least 20 items available.

Ordinary Java construction — statement excerpt
CatalogSettings settings = new CatalogSettings("INR", 20);

CatalogSettings holds these choices. ProductService uses minimumStock to choose products; the controller supplies the settings to the page for display. CatalogConfig specifies how the settings object is constructed. Separating those responsibilities is separation of concerns. A separate settings class is optional for this small application; we chose it as a concrete example of configuring an object with values.

NameWhat it means here
CatalogSettingsThe ordinary Java class holding currency and minimumStock.
CatalogConfigThe configuration class containing our factory methods.
Factory methodA method that produces and returns an object reference.
BeanThe actual object managed by Spring. Bean does not mean separation of concerns.

Is there a disadvantage to @Component on ProductFileReader?

There is no problem with @Component here. Stage 12 still uses it on ProductFileReader. What changes is where its JsonMapper helper comes from. Earlier, the reader built a mapper inside its constructor. Now, it asks for one through its constructor and Spring supplies the configured mapper bean.

CatalogConfig.java — mapper factory excerpt
@Bean
public JsonMapper jsonMapper() {
    return JsonMapper.builder().build();
}

JsonMapper is a library class. We cannot edit that dependency class in our project to put @Component on it. An @Bean method lets us build its object and give it to Spring. @Component suits classes we own and want Spring to discover; @Bean suits explicit construction, including objects from libraries. Neither approach is a replacement for the other.

Can we share a JsonMapper without Spring?

Yes. Spring is not required to create an object once and share its reference. The following illustrative statements use the Stage 12 ProductFileReader constructor. They demonstrate ordinary Java, not additional application startup code:

Plain Java sharing — illustrative statements
JsonMapper mapper = JsonMapper.builder().build();
ProductFileReader firstReader = new ProductFileReader(mapper);
ProductFileReader secondReader = new ProductFileReader(mapper);

There are two reader objects but both receive the same mapper object. By contrast, independently calling JsonMapper.builder().build() twice creates two mapper objects. Manually sharing one instance is possible; it does not itself enforce the singleton design pattern.

With @Bean, Spring manages the sharing for us. Its default singleton scope means one instance for that bean definition in that application context. It does not mean that Java prevents other mapper objects from being created. Calling the builder yourself still creates a separate object.

What does @Component actually tell Spring?

@Component marks a class for discovery by component scanning. When that class is discovered and registered, Spring creates and manages its object and resolves the required dependencies. In our example, read the startup sequence as:

  1. Spring discovers ProductFileReader in the scanned application package.
  2. It sees that the reader constructor requires a JsonMapper.
  3. It obtains the mapper bean from the configured factory method.
  4. It calls the reader constructor with that mapper reference.
  5. It keeps the reader bean available for other beans that need it.
Mapper factory returns an object → Spring supplies that mapper → Reader constructor receives it

@Component does not create every object the application might ever need. Each required dependency must be available through component discovery, explicit configuration or another Spring registration mechanism. Spring reports a startup error if it cannot resolve a required dependency.

Our default, non-lazy singleton beans are created during application-context startup. Spring also supports deferred creation and other scopes, so not every possible object must be created at startup. We do not need those additional features to understand this stage.

Say it in your own words: I can create and share objects myself. Spring can take responsibility for creating, managing and supplying the objects I register. @Component and @Bean are two ways of telling Spring about those objects.

Explore factory methods and @Bean in the third column.

CatalogConfig factory methods → JsonMapper and CatalogSettings beans → injected consumers
  1. Use a standalone file-based example

    This stage builds on Stage 10 so you can focus on bean configuration without starting MongoDB. It runs on 8090 and retains the original getInputStream() and try-with-resources reading method.

  2. Separate a class from its managed object

    CatalogSettings is an ordinary class with currency and minimumStock fields. It has no @Component annotation. new CatalogSettings("INR", 20) creates an ordinary Java object. Returning that object from an @Bean method lets Spring register and manage that particular object as a bean.

  3. Read the factory methods

    CatalogConfig has @Configuration. Its jsonMapper() method builds and returns a library JsonMapper. Its catalogSettings() method constructs and returns our settings object. A factory method is simply a method whose job is to produce an object. Here @Bean tells Spring to call these methods and register their returned objects. The default bean names are the method names.

  4. Inject the results

    ProductFileReader asks for JsonMapper in its constructor instead of building its own mapper. ProductService asks for CatalogSettings and uses minimumStock as the filtering threshold. The controller adds settings to Model so the page can show INR and 20. These are references to the managed objects, not copies of their source classes.

  5. Change one setting and observe the rule

    All products shows 30 rows. The policy link selects products whose stock is at least 20, including exactly 20. Change the factory’s minimumStock from 20 to 30, restart and compare the filtered page. A code change takes effect after rebuilding/restarting.

  6. Inspect the registry and scope

    Open /bean-info. The page checks bean definitions for jsonMapper, catalogSettings, productFileReader, fileProductRepository, productService and productController. A bean definition is the container’s registration information; a bean is the actual object. The registry keeps these named definitions. Two context lookups of catalogSettings return the same object because its default scope is singleton.

  7. Avoid accidental extra objects

    @Configuration(proxyBeanMethods=false) is used here. Calling a factory method directly in ordinary Java would be an ordinary method call. This example obtains the managed objects through constructor injection and context lookup, rather than calling one factory method from another.

Run this stage

Use JDK 21+ and Maven 3.9+. Extract spring-products-bean-config.zip and open a terminal in the spring-products-bean-config folder containing pom.xml. Keep the source files in their supplied paths.

Start the application
mvn spring-boot:run

Open http://localhost:8090/products. Keep the terminal open. Stop with Ctrl+C. If the port is occupied, change server.port in src/main/resources/application.properties, restart and use the new port in the browser.

src/main/resources/application.properties
# Choose another available port if this port is busy.
server.port=8090

Expected output and checks

Open /products and confirm currency INR and minimum stock 20. Use the policy link. Open /bean-info: all six registrations and singleton checks are true, and the two settings lookups are identical. Stop the app and change the threshold to 30 for the exercise.

Complete project files

Every project file is reproduced below. Create each file at the indicated path if building manually.

.gitignore
.gitignore
/target/
pom.xml
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.1</version>
    <relativePath/>
  </parent>
  <groupId>com.edupoly</groupId>
  <artifactId>spring-products-bean-config</artifactId>
  <version>1.0.0</version>
  <properties>
    <java.version>21</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
src/main/java/com/edupoly/products/BeanInfoController.java
src/main/java/com/edupoly/products/BeanInfoController.java
package com.edupoly.products;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class BeanInfoController {
    private final ConfigurableApplicationContext context;

    public BeanInfoController(ConfigurableApplicationContext context) {
        this.context = context;
    }

    @GetMapping(value = "/bean-info", produces = "text/plain")
    @ResponseBody
    public String beanInfo() {
        StringBuilder text = new StringBuilder();
        for (String name : new String[] {"jsonMapper", "catalogSettings", "productFileReader",
                "fileProductRepository", "productService", "productController"}) {
            text.append(name)
                .append(" | definition registered: ").append(context.containsBeanDefinition(name))
                .append(" | singleton: ").append(context.isSingleton(name)).append("\n");
        }
        text.append("Same settings bean on two lookups: ")
            .append(context.getBean(CatalogSettings.class) == context.getBean(CatalogSettings.class));
        return text.toString();
    }
}
src/main/java/com/edupoly/products/CatalogConfig.java
src/main/java/com/edupoly/products/CatalogConfig.java
package com.edupoly.products;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.databind.json.JsonMapper;

@Configuration(proxyBeanMethods = false)
public class CatalogConfig {
    @Bean
    public JsonMapper jsonMapper() {
        return JsonMapper.builder().build();
    }

    @Bean
    public CatalogSettings catalogSettings() {
        return new CatalogSettings("INR", 20);
    }
}
src/main/java/com/edupoly/products/CatalogSettings.java
src/main/java/com/edupoly/products/CatalogSettings.java
package com.edupoly.products;

public class CatalogSettings {
    private final String currency;
    private final int minimumStock;

    public CatalogSettings(String currency, int minimumStock) {
        this.currency = currency;
        this.minimumStock = minimumStock;
    }

    public String getCurrency() { return currency; }
    public int getMinimumStock() { return minimumStock; }
}
src/main/java/com/edupoly/products/FileProductRepository.java
src/main/java/com/edupoly/products/FileProductRepository.java
package com.edupoly.products;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;

@Repository
public class FileProductRepository implements ProductRepository {
    private final ProductFileReader reader;

    public FileProductRepository(ProductFileReader reader) {
        this.reader = reader;
    }

    @Override
    public List<Product> findAll() {
        return Arrays.asList(reader.getProducts());
    }

    @Override
    public Optional<Product> findById(int id) {
        for (Product product : reader.getProducts()) {
            if (product.getId() == id) {
                return Optional.of(product);
            }
        }
        return Optional.empty();
    }
}
src/main/java/com/edupoly/products/Product.java
src/main/java/com/edupoly/products/Product.java
package com.edupoly.products;

import java.math.BigDecimal;

public class Product {
    private int id;
    private String name;
    private String category;
    private BigDecimal price;
    private String brand;
    private int stock;
    private String description;
    private int warrantyMonths;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getWarrantyMonths() {
        return warrantyMonths;
    }

    public void setWarrantyMonths(int warrantyMonths) {
        this.warrantyMonths = warrantyMonths;
    }
}
src/main/java/com/edupoly/products/ProductApplication.java
src/main/java/com/edupoly/products/ProductApplication.java
package com.edupoly.products;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}
src/main/java/com/edupoly/products/ProductController.java
src/main/java/com/edupoly/products/ProductController.java
package com.edupoly.products;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;

@Controller
public class ProductController {
    private final ProductService service;
    private final CatalogSettings settings;

    public ProductController(ProductService service, CatalogSettings settings) {
        this.service = service;
        this.settings = settings;
    }

    @GetMapping({"/", "/products"})
    public String listProducts(
            @RequestParam(name = "inStockOnly", defaultValue = "false") boolean inStockOnly,
            Model model) {
        model.addAttribute("products", service.findAll(inStockOnly));
        model.addAttribute("settings", settings);
        return "products";
    }

    @GetMapping("/products/{id}")
    public String productDetails(@PathVariable("id") int id, Model model) {
        Product product = service.findById(id).orElseThrow(() ->
                new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found"));
        model.addAttribute("product", product);
        return "product-details";
    }
}
src/main/java/com/edupoly/products/ProductFileReader.java
src/main/java/com/edupoly/products/ProductFileReader.java
package com.edupoly.products;

import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import tools.jackson.databind.json.JsonMapper;

@Component
public class ProductFileReader {
    private final Product[] products;

    public ProductFileReader(JsonMapper mapper) throws IOException {
        var resource = new ClassPathResource("data/products.txt");
        try (var input = resource.getInputStream()) {
            products = mapper.readValue(input, Product[].class);
        }
        System.out.println("Created ProductFileReader");
    }

    public Product[] getProducts() {
        return products;
    }
}
src/main/java/com/edupoly/products/ProductRepository.java
src/main/java/com/edupoly/products/ProductRepository.java
package com.edupoly.products;

import java.util.List;
import java.util.Optional;

public interface ProductRepository {
    List<Product> findAll();
    Optional<Product> findById(int id);
}
src/main/java/com/edupoly/products/ProductService.java
src/main/java/com/edupoly/products/ProductService.java
package com.edupoly.products;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    private final ProductRepository repository;
    private final CatalogSettings settings;

    public ProductService(ProductRepository repository, CatalogSettings settings) {
        this.repository = repository;
        this.settings = settings;
    }

    public List<Product> findAll(boolean inStockOnly) {
        List<Product> selected = new ArrayList<>();
        for (Product product : repository.findAll()) {
            if (!inStockOnly || product.getStock() >= settings.getMinimumStock()) {
                selected.add(product);
            }
        }
        return selected;
    }

    public Optional<Product> findById(int id) {
        return repository.findById(id);
    }
}
src/main/resources/application.properties
src/main/resources/application.properties
# Choose another available port if this port is busy.
server.port=8090
src/main/resources/data/products.txt
src/main/resources/data/products.txt
[
  {
    "id": 1,
    "name": "Wireless Mouse",
    "category": "Electronics",
    "price": 699,
    "brand": "EduTech",
    "stock": 12,
    "description": "Wireless Mouse for everyday learning, study and work. Product 1 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 2,
    "name": "Mechanical Keyboard",
    "category": "Electronics",
    "price": 3499,
    "brand": "EduTech",
    "stock": 19,
    "description": "Mechanical Keyboard for everyday learning, study and work. Product 2 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 3,
    "name": "USB-C Hub",
    "category": "Electronics",
    "price": 1599,
    "brand": "EduTech",
    "stock": 26,
    "description": "USB-C Hub for everyday learning, study and work. Product 3 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 4,
    "name": "Laptop Stand",
    "category": "Electronics",
    "price": 1299,
    "brand": "EduTech",
    "stock": 33,
    "description": "Laptop Stand for everyday learning, study and work. Product 4 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 5,
    "name": "Webcam",
    "category": "Electronics",
    "price": 2199,
    "brand": "EduTech",
    "stock": 0,
    "description": "Webcam for everyday learning, study and work. Product 5 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 6,
    "name": "Bluetooth Speaker",
    "category": "Electronics",
    "price": 2499,
    "brand": "EduTech",
    "stock": 47,
    "description": "Bluetooth Speaker for everyday learning, study and work. Product 6 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 7,
    "name": "Noise-Cancelling Headphones",
    "category": "Electronics",
    "price": 4999,
    "brand": "EduTech",
    "stock": 54,
    "description": "Noise-Cancelling Headphones for everyday learning, study and work. Product 7 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 8,
    "name": "Portable SSD",
    "category": "Electronics",
    "price": 6499,
    "brand": "EduTech",
    "stock": 61,
    "description": "Portable SSD for everyday learning, study and work. Product 8 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 9,
    "name": "USB Flash Drive",
    "category": "Electronics",
    "price": 599,
    "brand": "EduTech",
    "stock": 68,
    "description": "USB Flash Drive for everyday learning, study and work. Product 9 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 10,
    "name": "Monitor",
    "category": "Electronics",
    "price": 12999,
    "brand": "EduTech",
    "stock": 15,
    "description": "Monitor for everyday learning, study and work. Product 10 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 11,
    "name": "Desk Lamp",
    "category": "Electronics",
    "price": 899,
    "brand": "EduTech",
    "stock": 22,
    "description": "Desk Lamp for everyday learning, study and work. Product 11 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 12,
    "name": "Power Bank",
    "category": "Electronics",
    "price": 1799,
    "brand": "EduTech",
    "stock": 29,
    "description": "Power Bank for everyday learning, study and work. Product 12 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 13,
    "name": "Smartphone Stand",
    "category": "Electronics",
    "price": 399,
    "brand": "EduTech",
    "stock": 36,
    "description": "Smartphone Stand for everyday learning, study and work. Product 13 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 14,
    "name": "HDMI Cable",
    "category": "Electronics",
    "price": 499,
    "brand": "EduTech",
    "stock": 43,
    "description": "HDMI Cable for everyday learning, study and work. Product 14 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 15,
    "name": "Ethernet Adapter",
    "category": "Electronics",
    "price": 999,
    "brand": "EduTech",
    "stock": 50,
    "description": "Ethernet Adapter for everyday learning, study and work. Product 15 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 16,
    "name": "Graphics Tablet",
    "category": "Electronics",
    "price": 3999,
    "brand": "EduTech",
    "stock": 57,
    "description": "Graphics Tablet for everyday learning, study and work. Product 16 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 17,
    "name": "Microphone",
    "category": "Electronics",
    "price": 2999,
    "brand": "EduTech",
    "stock": 64,
    "description": "Microphone for everyday learning, study and work. Product 17 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 18,
    "name": "Laptop Backpack",
    "category": "Electronics",
    "price": 1499,
    "brand": "EduTech",
    "stock": 71,
    "description": "Laptop Backpack for everyday learning, study and work. Product 18 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 19,
    "name": "Screen Cleaner",
    "category": "Electronics",
    "price": 249,
    "brand": "EduTech",
    "stock": 18,
    "description": "Screen Cleaner for everyday learning, study and work. Product 19 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 20,
    "name": "Mouse Pad",
    "category": "Electronics",
    "price": 299,
    "brand": "EduTech",
    "stock": 25,
    "description": "Mouse Pad for everyday learning, study and work. Product 20 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 21,
    "name": "Notebook",
    "category": "Study Essentials",
    "price": 149,
    "brand": "StudyWorks",
    "stock": 32,
    "description": "Notebook for everyday learning, study and work. Product 21 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 22,
    "name": "Ballpoint Pen Set",
    "category": "Study Essentials",
    "price": 199,
    "brand": "StudyWorks",
    "stock": 39,
    "description": "Ballpoint Pen Set for everyday learning, study and work. Product 22 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 23,
    "name": "Water Bottle",
    "category": "Study Essentials",
    "price": 599,
    "brand": "StudyWorks",
    "stock": 46,
    "description": "Water Bottle for everyday learning, study and work. Product 23 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 24,
    "name": "Desk Organizer",
    "category": "Study Essentials",
    "price": 449,
    "brand": "StudyWorks",
    "stock": 53,
    "description": "Desk Organizer for everyday learning, study and work. Product 24 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 25,
    "name": "Reading Light",
    "category": "Study Essentials",
    "price": 699,
    "brand": "StudyWorks",
    "stock": 60,
    "description": "Reading Light for everyday learning, study and work. Product 25 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 26,
    "name": "Calculator",
    "category": "Study Essentials",
    "price": 499,
    "brand": "StudyWorks",
    "stock": 67,
    "description": "Calculator for everyday learning, study and work. Product 26 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 27,
    "name": "Whiteboard",
    "category": "Study Essentials",
    "price": 1299,
    "brand": "StudyWorks",
    "stock": 14,
    "description": "Whiteboard for everyday learning, study and work. Product 27 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 28,
    "name": "Marker Set",
    "category": "Study Essentials",
    "price": 299,
    "brand": "StudyWorks",
    "stock": 21,
    "description": "Marker Set for everyday learning, study and work. Product 28 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 29,
    "name": "Book Stand",
    "category": "Study Essentials",
    "price": 799,
    "brand": "StudyWorks",
    "stock": 28,
    "description": "Book Stand for everyday learning, study and work. Product 29 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 30,
    "name": "Travel Mug",
    "category": "Study Essentials",
    "price": 649,
    "brand": "StudyWorks",
    "stock": 35,
    "description": "Travel Mug for everyday learning, study and work. Product 30 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  }
]
src/main/resources/static/style.css
src/main/resources/static/style.css
body{margin:0;padding:28px 16px;background:#f5f7fb;color:#17253d;font:16px/1.6 system-ui,sans-serif}main{max-width:1100px;margin:20px auto;padding:28px;background:#fff;border:1px solid #dbe2ec;border-radius:12px}h1{margin-top:0}a{color:#2457a6;text-underline-offset:3px}.table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:12px;border-bottom:1px solid #dbe2ec}th{background:#edf3fc}tbody tr:hover{background:#f8faff}.detail{max-width:760px}dl{display:grid;grid-template-columns:160px minmax(0,1fr);gap:12px 20px;margin:24px 0}dt{font-weight:650}dd{margin:0;overflow-wrap:anywhere}@media(max-width:500px){main{padding:16px}dl{grid-template-columns:1fr;gap:4px}dd{margin-bottom:12px}}

.catalog-nav{margin:20px 0}form{margin:18px 0}button{padding:9px 14px;cursor:pointer}input{padding:8px;max-width:140px}form button{margin-left:8px}
src/main/resources/templates/product-details.html
src/main/resources/templates/product-details.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title th:text="${product.name}">Product details</title>
    <link rel="stylesheet" th:href="@{/style.css}">
</head>
<body>
<main class="detail">
    <h1>Product details</h1>
    <h2 th:text="${product.name}">Wireless Mouse</h2>
    <dl>
        <dt>ID</dt><dd th:text="${product.id}">1</dd>
        <dt>Name</dt><dd th:text="${product.name}">Wireless Mouse</dd>
        <dt>Category</dt><dd th:text="${product.category}">Electronics</dd>
        <dt>Price (INR)</dt><dd th:text="${product.price}">699</dd>
        <dt>Brand</dt><dd th:text="${product.brand}">EduTech</dd>
        <dt>Stock</dt><dd th:text="${product.stock}">12</dd>
        <dt>Description</dt><dd th:text="${product.description}">Product description</dd>
        <dt>Warranty (months)</dt><dd th:text="${product.warrantyMonths}">12</dd>
    </dl>
    <a th:href="@{/products}">Back to products</a>
</main>
</body>
</html>
src/main/resources/templates/products.html
src/main/resources/templates/products.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Products</title>
    <link rel="stylesheet" th:href="@{/style.css}">
</head>
<body>
<main>
    <h1>Products</h1>
    <p>Stage 12: Explicitly configured Spring beans. Select More details for a product.</p>
    <p>Currency: <strong th:text="${settings.currency}">INR</strong>. Minimum stock for the policy: <strong th:text="${settings.minimumStock}">20</strong>.</p>
    <p><a th:href="@{/bean-info}">Inspect the registered beans</a></p>
    <nav class="catalog-nav"><a th:href="@{/products}">All products</a> · <a th:href="@{/products(inStockOnly=true)}">Products meeting the stock policy</a></nav>
    <div class="table-scroll">
    <table>
        <thead>
        <tr><th>ID</th><th>Name</th><th>Category</th><th>Price (INR)</th><th>Action</th></tr>
        </thead>
        <tbody>
        <tr th:each="product : ${products}">
            <td th:text="${product.id}">1</td>
            <td th:text="${product.name}">Wireless Mouse</td>
            <td th:text="${product.category}">Electronics</td>
            <td th:text="${product.price}">699</td>
            <td><a th:href="@{/products/{id}(id=${product.id})}">More details</a></td>
        </tr>
        </tbody>
    </table>
    </div>
</main>
</body>
</html>

Practice

Change minimumStock from 20 to 30, restart and compare the filtered list. Explain why CatalogSettings is a bean even though its class has no @Component.

References: Spring constructor injection, Spring Data MongoDB repositories.

After Stage 12: the Spring words to recognize

TermOur concrete exampleIts place in Spring
@ConfigurationCatalogConfigDeclares a class containing explicit bean configuration.
@Bean and factory methodcatalogSettings() returns new CatalogSettings(...)Spring registers the object returned by this method.
Bean definition / registryThe named catalogSettings registrationThe container keeps information describing how to obtain a managed object.
Singleton — observed againTwo lookups return the same settings referenceOne instance for this bean definition in this context by default.

One small check: Point to the class, constructor or annotation that demonstrates each term before continuing.

Continue learning

  1. Stage 8: Let Spring supply a product-file reader — complete project ZIP
  2. Stage 9: Move a product rule into a service — complete project ZIP
  3. Stage 10: Put data access behind a repository — complete project ZIP
  4. Stage 11: Store and change products in MongoDB — complete project ZIP
  5. Stage 12: Create beans with explicit factory methods — complete project ZIP
  6. Stage 13: MongoDB connectivity — complete project ZIP
  7. Stage 14: Todo list application — complete project ZIP

1. Connect the new concepts to familiar ones