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.
Learning path / Stage 17 of 37
mvn spring-boot:run, then open http://localhost:8117/products. Stop with Ctrl+C. The port is declared in application.properties.Add delivery details to the existing product page, then discover why package organisation affects Spring bean discovery.
Download the complete Stage 17 project · Open the project guide
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.
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 source | What changes |
|---|---|
DeliveryInfoService.java | New service: supply the fixed delivery information. |
Application.java | Include the application and delivery packages in component scanning. |
ProductController.java | Receive the delivery service and pass its values to Model. |
product-details.html | Display the delivery information below the product details. |
style.css | Draw attention to the delivery information with a red border. |
Application.java
@SpringBootApplication(scanBasePackages = {"com.edupoly.lesson", "com.edupoly.delivery"})ProductController.java
public ProductController(ProductService service, DeliveryInfoService delivery) {
this.service = service;
this.delivery = delivery;
}ProductController.java
model.addAttribute("deliveryTime", delivery.deliveryTime());
model.addAttribute("deliveryCharge", delivery.deliveryCharge());DeliveryInfoService.java
package com.edupoly.delivery;product-details.html
<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
- Move DeliveryInfoService.java from src/main/java/com/edupoly/delivery/ to src/main/java/com/edupoly/lesson/.
- Change its package declaration to package com.edupoly.lesson;.
- Remove import com.edupoly.delivery.DeliveryInfoService; from ProductController.java: the classes now share a package.
- 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.
- In Application.java replace the annotation with plain @SpringBootApplication.
- 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
- Move the service back to src/main/java/com/edupoly/delivery/ and restore package com.edupoly.delivery;.
- Restore import com.edupoly.delivery.DeliveryInfoService; in ProductController.java.
- In LessonTest.java restore both type references to com.edupoly.delivery.DeliveryInfoService.
- Keep plain @SpringBootApplication. Run mvn clean package -DskipTests: Java compilation and packaging succeed.
- 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.
@SpringBootApplication(scanBasePackages = {"com.edupoly.lesson", "com.edupoly.delivery"})What Spring does
- Reads the configured component-scan packages.
- Finds eligible classes such as @Controller and @Service and registers their bean definitions.
- Creates ProductController with the required service references, obtaining its dependencies first.
- 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.
What we change and why
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.
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.
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.
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.
mvn test
mvn spring-boot:runOpen 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
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
<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
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
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.
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
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
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
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
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
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.
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.
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
[
{
"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.
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.
<!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
<!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
<!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
server.port=8117
spring.application.name=spring-stage-17-scan-packages
src/test/java/com/edupoly/lesson/LessonTest.java
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
| Word | Meaning in this example |
|---|---|
| Component scanning — new focus | Spring searches selected packages for eligible component classes and registers their bean definitions. |
| Scan boundary | The main-class package and its descendants are the default component-scan area in this application. |
| scanBasePackages | Explicitly includes the lesson and delivery packages for component discovery. |
| @Service — reused | Marks DeliveryInfoService as an eligible specialised component. |
| Constructor injection and Model — reused | Supply 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.
Concepts used in this stage
Use the right column for a quick reminder, or open a concept above for its full explanation.