Stage 17: show delivery information from a service in another package

Reused: constructor injection. New: component-scan boundaries.

Visible requirement: add delivery information to each product-details page. We write a new class, DeliveryInfoService, to supply that information. It is our Java class, not a class supplied by Spring.

The controller receives it through the constructor, using the mechanism from Stage 15. The new question is: how does Spring find this service when we organise it in a different package?

We continue the 30-product application, including the Stage 16 report. The supplied ZIP contains the complete working version. The guided experiment below first uses the default scan, then deliberately breaks discovery, then fixes it.

Add delivery details to the existing product page, then discover why package organisation affects Spring bean discovery.

Open this stage in the online learning path

Before you start

Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.

Continue the Stage 16 product application. Start with the delivery feature and diagram, then inspect the five changed files and try the scan-boundary experiment.

The practical requirement

When a customer opens More details for a product, show the standard delivery time and charge. DeliveryInfoService supplies the fixed values “3-5 working days” and “49”. These are demonstration delivery terms, not a courier integration or a calculated promise based on an address.

Product delivery panel in a red box, default versus explicit package scans, and the request flow
The UI feature and the discovery that makes it possible. Open the full-size diagram.
GET /products/1 → ProductController → DeliveryInfoService → Model → product-details.html

Where delivery is added: five files

The product data, repository, product service and report files are reused. application.properties also declares this stage's port, 8117; it is a run setting rather than a delivery-feature change.

File — jump to complete sourceWhat changes
DeliveryInfoService.javaNew service: supply the fixed delivery information.
Application.javaInclude the application and delivery packages in component scanning.
ProductController.javaReceive the delivery service and pass its values to Model.
product-details.htmlDisplay the delivery information below the product details.
style.cssDraw attention to the delivery information with a red border.
Blue = familiar constructor and ModelYellow = delivery feature and scan boundary

Application.java

Application.java · focus lines
@SpringBootApplication(scanBasePackages = {"com.edupoly.lesson", "com.edupoly.delivery"})

ProductController.java

ProductController.java · focus lines
    public ProductController(ProductService service, DeliveryInfoService delivery) {
        this.service = service;
        this.delivery = delivery;
    }

ProductController.java

ProductController.java · focus lines
                model.addAttribute("deliveryTime", delivery.deliveryTime());
                model.addAttribute("deliveryCharge", delivery.deliveryCharge());

DeliveryInfoService.java

DeliveryInfoService.java · focus lines
package com.edupoly.delivery;

product-details.html

product-details.html · focus lines
    <section class="delivery-info" id="delivery-info">
        <h2>Delivery information</h2>
        <p>Standard delivery: <span th:text="${deliveryTime}">3-5 working days</span></p>
        <p>Delivery charges: INR <span th:text="${deliveryCharge}">49</span></p>
    </section>

Five complete files below have yellow DELIVERY FEATURE headings. Constructor injection and Model are reused; explicitly setting the component-scan boundary is the new mechanism.

Guided experiment: working, missing, discovered

The download starts in the final working state. First run it unchanged. Then stop the application with Ctrl+C before each edit and restart it after each step. Do not keep two copies of DeliveryInfoService.

A. Place the service inside the default boundary

  1. Move DeliveryInfoService.java from src/main/java/com/edupoly/delivery/ to src/main/java/com/edupoly/lesson/.
  2. Change its package declaration to package com.edupoly.lesson;.
  3. Remove import com.edupoly.delivery.DeliveryInfoService; from ProductController.java: the classes now share a package.
  4. In src/test/java/com/edupoly/lesson/LessonTest.java replace both occurrences of com.edupoly.delivery.DeliveryInfoService with com.edupoly.lesson.DeliveryInfoService. Tests must refer to the moved Java type too.
  5. In Application.java replace the annotation with plain @SpringBootApplication.
  6. Run mvn clean spring-boot:run and open /products/1. The delivery panel appears because both classes are in com.edupoly.lesson.

B. Move delivery into its own package

  1. Move the service back to src/main/java/com/edupoly/delivery/ and restore package com.edupoly.delivery;.
  2. Restore import com.edupoly.delivery.DeliveryInfoService; in ProductController.java.
  3. In LessonTest.java restore both type references to com.edupoly.delivery.DeliveryInfoService.
  4. Keep plain @SpringBootApplication. Run mvn clean package -DskipTests: Java compilation and packaging succeed.
  5. Run mvn spring-boot:run. Application startup fails: ProductController requires a DeliveryInfoService bean that the default scan did not discover. The existing Java import does not register a bean.

clean removes stale compiled classes after a package move. The lesson tests intentionally check the final package arrangement; use -DskipTests only for this intermediate packaging demonstration.

C. Include both packages

Restore the annotation below in Application.java, then run mvn clean test and mvn spring-boot:run. Open /products/1: the delivery panel returns. You are back to the supplied project layout.

Application.java · final working configuration
@SpringBootApplication(scanBasePackages = {"com.edupoly.lesson", "com.edupoly.delivery"})

What Spring does

  1. Reads the configured component-scan packages.
  2. Finds eligible classes such as @Controller and @Service and registers their bean definitions.
  3. Creates ProductController with the required service references, obtaining its dependencies first.
  4. Later, a request calls the controller; it obtains delivery values and passes them to the template through Model.

Import versus discovery: a Java import lets source code refer to a type. It does not cause Spring to manage an instance. @Service marks a class as an eligible component; the component scan must reach its package.

@SpringBootApplication already includes component-scanning behavior. scanBasePackages changes that behavior; we do not need an additional @ComponentScan here. Explicit component scanning is also available in plain Spring configuration through @ComponentScan.

Another valid layout: in a new application, place Application in a common root such as com.edupoly, with lesson and delivery beneath it. The default scan then reaches both. Here we keep the established main-class package so the boundary and its explicit fix remain visible. This setting controls component scanning; it is not a universal setting for database entity or repository scanning.

Spring Boot package-layout guidance

What we change and why

  1. Introduce the service by its job

    DeliveryInfoService is a new Java class we write. It supplies fixed delivery terms. The @Service annotation is familiar; the package com.edupoly.delivery is the new location we want Spring to discover.

  2. Reuse the existing request flow

    ProductController still receives ProductService. It also receives DeliveryInfoService through its constructor. On the details request it adds deliveryTime and deliveryCharge to Model. The template displays those values with familiar th:text attributes.

  3. Organise related code together

    Application and the product classes remain in com.edupoly.lesson. The new delivery service belongs to the sibling com.edupoly.delivery package. A sibling package is not a descendant of the default scan root.

  4. Make the scan boundary explicit

    Application.java includes both packages in scanBasePackages. Spring can now discover the @Service class, register its bean and supply it to the controller. This is startup wiring; opening the product page does not trigger a new package scan.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · Windows, macOS and Linux
mvn test
mvn spring-boot:run

Open http://localhost:8117/products. Keep the terminal running. Stop with Ctrl+C. If the port is occupied, edit server.port in application.properties before restarting.

Expected result

Expected output · ignore framework startup messages
Open http://localhost:8117/products
Total products: 30
Click More details on the first product
Product: Wireless Mouse
Delivery information
Standard delivery: 3-5 working days
Delivery charges: INR 49
The Stage 16 report remains available at /report.

Understand each complete file

Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.

pom.xml
pom.xml · complete file
<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-stage-17-scan-packages</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version></properties>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
    <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/lesson/Notifier.java
src/main/java/com/edupoly/lesson/Notifier.java · complete file
package com.edupoly.lesson;

public class Notifier {
    public String message(int productCount) {
        return "Catalog report prepared for " + productCount + " products.";
    }
}
src/main/java/com/edupoly/lesson/Product.java
src/main/java/com/edupoly/lesson/Product.java · complete file
package com.edupoly.lesson;

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;
    }
}
DELIVERY FEATUREsrc/main/java/com/edupoly/lesson/ProductController.java

Receive the delivery service and pass its values to Model.

src/main/java/com/edupoly/lesson/ProductController.java · complete file
package com.edupoly.lesson;

import com.edupoly.delivery.DeliveryInfoService;
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.server.ResponseStatusException;

@Controller
public class ProductController {
    private final ProductService service;

    private final DeliveryInfoService delivery;

    public ProductController(ProductService service, DeliveryInfoService delivery) {
        this.service = service;
        this.delivery = delivery;
    }

    @GetMapping({"/", "/products"})
    public String products(Model model) {
        model.addAttribute("products", service.findAll());
        model.addAttribute("totalProducts", service.count());
        return "products";
    }

    @GetMapping("/products/{id}")
    public String details(@PathVariable("id") int id, Model model) {
        for (Product product : service.findAll()) {
            if (product.getId() == id) {
                model.addAttribute("product", product);
                model.addAttribute("deliveryTime", delivery.deliveryTime());
                model.addAttribute("deliveryCharge", delivery.deliveryCharge());
                return "product-details";
            }
        }
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
    }
}
src/main/java/com/edupoly/lesson/ProductRepository.java
src/main/java/com/edupoly/lesson/ProductRepository.java · complete file
package com.edupoly.lesson;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Repository;
import tools.jackson.databind.json.JsonMapper;

@Repository
public class ProductRepository {
    private final Product[] products;

    public ProductRepository() throws IOException {
        var resource = new ClassPathResource("data/products.txt");
        try (var input = resource.getInputStream()) {
            JsonMapper mapper = JsonMapper.builder().build();
            products = mapper.readValue(input, Product[].class);
        }
    }

    public Product[] findAll() { return products; }

    public List<String> titles() {
        List<String> titles = new ArrayList<>();
        for (Product product : products) {
            titles.add(product.getName());
        }
        return titles;
    }
}
src/main/java/com/edupoly/lesson/ProductService.java
src/main/java/com/edupoly/lesson/ProductService.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Service;

@Service
public class ProductService {
    private final ProductRepository repository;

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

    public Product[] findAll() { return repository.findAll(); }

    public int count() {
        return repository.titles().size();
    }
}
src/main/java/com/edupoly/lesson/ReportConfig.java
src/main/java/com/edupoly/lesson/ReportConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReportConfig {
    @Bean
    public Notifier notifier() {
        return new Notifier();
    }
}
src/main/java/com/edupoly/lesson/ReportController.java
src/main/java/com/edupoly/lesson/ReportController.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ReportController {
    private final ReportService reports;

    public ReportController(ReportService reports) {
        this.reports = reports;
    }

    @GetMapping("/report")
    public String report(Model model) {
        model.addAttribute("totalProducts", reports.productCount());
        model.addAttribute("reportMessage", reports.message());
        return "report";
    }
}
src/main/java/com/edupoly/lesson/ReportService.java
src/main/java/com/edupoly/lesson/ReportService.java · complete file
package com.edupoly.lesson;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ReportService {
    private final ProductService products;

    public ReportService(ProductService products) {
        this.products = products;
    }

    private Notifier notifier;

    @Autowired(required = false)
    public void setNotifier(Notifier notifier) {
        this.notifier = notifier;
    }

    public int productCount() { return products.count(); }

    public String message() {
        if (notifier == null) return "";
        return notifier.message(productCount());
    }
}
DELIVERY FEATUREsrc/main/java/com/edupoly/delivery/DeliveryInfoService.java

New service: supply the fixed delivery information.

src/main/java/com/edupoly/delivery/DeliveryInfoService.java · complete file
package com.edupoly.delivery;

import org.springframework.stereotype.Service;

@Service
public class DeliveryInfoService {
    public String deliveryTime() { return "3-5 working days"; }
    public int deliveryCharge() { return 49; }
}
DELIVERY FEATUREsrc/main/java/com/edupoly/lesson/Application.java

Include the application and delivery packages in component scanning.

src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

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

@SpringBootApplication(scanBasePackages = {"com.edupoly.lesson", "com.edupoly.delivery"})
public class Application {
    public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}
src/main/resources/data/products.txt
src/main/resources/data/products.txt · complete file
[
  {
    "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": 40,
    "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
  }
]
DELIVERY FEATUREsrc/main/resources/static/style.css

Draw attention to the delivery information with a red border.

src/main/resources/static/style.css · complete file
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}}

.brand{color:#164aa5;font-weight:700}.total-products{display:inline-block;border:3px solid #ef111f;border-radius:4px;padding:12px 18px;background:#fff7f7;color:#14213a;font-size:24px;font-weight:700;margin:4px 0 18px}

.report-action{display:inline-block;background:#164aa5;color:white;padding:10px 16px;border-radius:6px;text-decoration:none}.report-count{font-size:24px;font-weight:700;color:#164aa5}.report-message{background:#fff7f7;border:3px solid #ef111f;border-radius:4px;padding:16px 20px;color:#14213a}

.delivery-info{border:3px solid #ef111f;background:#fff7f7;padding:18px 22px;border-radius:6px;margin:24px 0}.delivery-info h2{margin-top:0}
DELIVERY FEATUREsrc/main/resources/templates/product-details.html

Display the delivery information below the product details.

src/main/resources/templates/product-details.html · complete file
<!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>
    <section class="delivery-info" id="delivery-info">
        <h2>Delivery information</h2>
        <p>Standard delivery: <span th:text="${deliveryTime}">3-5 working days</span></p>
        <p>Delivery charges: INR <span th:text="${deliveryCharge}">49</span></p>
    </section>
    <a th:href="@{/products}">Back to products</a>
</main>
</body>
</html>
src/main/resources/templates/products.html
src/main/resources/templates/products.html · complete file
<!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>
    <p class="brand">Edupoly · Product catalog</p>
    <h1>Products</h1>
    <p class="total-products" id="total-products">Total products: <strong th:text="${totalProducts}">30</strong></p>
    <p>Select More details to see a product's complete information. The red box shows the number of products in the catalog.</p>
    <p><a class="report-action" th:href="@{/report}">Generate product report</a></p>
    <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>
src/main/resources/templates/report.html
src/main/resources/templates/report.html · complete file
<!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>Product report</title><link rel="stylesheet" th:href="@{/style.css}"></head>
<body><main>
    <p class="brand">Edupoly · Product catalog</p>
    <h1>Product report</h1>
    <p class="report-count">Total products: <strong th:text="${totalProducts}">30</strong></p>
    <p class="report-message" id="report-message" th:if="${!reportMessage.isEmpty()}" th:text="${reportMessage}">Catalog report prepared.</p>
    <p><a th:href="@{/products}">Back to products</a></p>
</main></body></html>
src/main/resources/application.properties
src/main/resources/application.properties · complete file
server.port=8117
spring.application.name=spring-stage-17-scan-packages
src/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
package com.edupoly.lesson;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class LessonTest {
    @Test void deliveryServiceSuppliesTheDisplayedTerms() {
        var delivery = new com.edupoly.delivery.DeliveryInfoService();
        assertEquals("3-5 working days", delivery.deliveryTime());
        assertEquals(49, delivery.deliveryCharge());
    }

    @Test void requiredControllerFailsWhenDeliveryIsNotRegistered() {
        try (var context = new AnnotationConfigApplicationContext()) {
            context.register(ProductRepository.class, ProductService.class, ProductController.class);
            var error = assertThrows(org.springframework.beans.factory.UnsatisfiedDependencyException.class, context::refresh);
            assertTrue(error.getMessage().contains("DeliveryInfoService"));
        }
    }

    @Test void bootScanFindsDeliveryAndRendersTheProductPage() throws Exception {
        try (var context = org.springframework.boot.SpringApplication.run(Application.class, "--server.port=0")) {
            assertNotNull(context.getBean(com.edupoly.delivery.DeliveryInfoService.class));
            int port = context.getEnvironment().getProperty("local.server.port", Integer.class);
            var client = java.net.http.HttpClient.newHttpClient();
            String base = "http://127.0.0.1:" + port;
            var list = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/products")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(200, list.statusCode()); assertTrue(list.body().contains("<strong>30</strong>"));
            var page = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/products/1")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(200, page.statusCode());
            assertTrue(page.body().contains("id=\"delivery-info\""));
            assertTrue(page.body().contains("3-5 working days"));
            assertTrue(page.body().contains("<span>49</span>"));
            assertFalse(page.body().contains("th:text"));
            var missing = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/products/99999")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(404, missing.statusCode());
            var report = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/report")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(200, report.statusCode());
            assertTrue(report.body().contains("Catalog report prepared for 30 products."));
        }
    }
}

Try a failure, then correct it

Follow the guided A/B/C experiment. With the service in the sibling package and the default scan restored, compilation succeeds but startup fails because the required delivery bean is missing. Restore both scan packages and restart; the details page works.

Words you can now explain

WordMeaning in this example
Component scanning — new focusSpring searches selected packages for eligible component classes and registers their bean definitions.
Scan boundaryThe main-class package and its descendants are the default component-scan area in this application.
scanBasePackagesExplicitly includes the lesson and delivery packages for component discovery.
@Service — reusedMarks DeliveryInfoService as an eligible specialised component.
Constructor injection and Model — reusedSupply the service to the controller, then pass delivery values to the view.

Practice assignment

Change the delivery text to 2-4 working days and the charge to 39. Verify the details page reflects both values. Explain why restoring a Java import alone cannot fix a missing scan package, and why moving Application to the common root is another possible solution.

Read the official documentation →