Stage 9: Move a product rule into a service
Learning path / Stage 9 of 37
mvn spring-boot:run. Use the port specified for this stage and stop with Ctrl+C.Download spring-products-service.zip · Complete file-by-file guide
Keep request handling in the controller
The controller reads inStockOnly from the query string and supplies products to Model. It no longer decides which stock values qualify.
Put the rule in ProductService
findAll(false) returns every product. findAll(true) adds only products whose stock is greater than zero to the result list. That if condition is our business rule. @Service is a specialized component annotation that tells readers this class performs application operations.
Inject the required objects
Spring supplies ProductFileReader to ProductService and ProductService to ProductController. The browser never creates these Java objects. They are connected during application startup.
See an actual difference
This project deliberately changes product 5 in products.txt to stock 0. All products therefore shows 30 rows; In-stock products shows 29. The original Stage 6 project is unchanged. The query ?inStockOnly=true selects the filtered operation.
Read the lookup method
findById returns Optional<Product>. Optional is a Java type for a value that may be absent, not a Spring annotation. The controller turns an absent product into a 404 response.
Run this stage
Use JDK 21+ and Maven 3.9+. Extract spring-products-service.zip and open a terminal in the spring-products-service folder containing pom.xml. Keep the source files in their supplied paths.
mvn spring-boot:runOpen http://localhost:8087/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.
# Choose another available port if this port is busy.
server.port=8087
Expected output and checks
Compare /products (30 rows) and /products?inStockOnly=true (29 rows). Product 5 still has a details page with stock 0. An unknown ID returns 404.
Read the changed Java files
The downloadable project includes every file, the 30-product text file, templates, CSS, pom.xml and a standalone README.html with all source files. These are the key changes from the preceding example.
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 ProductFileReader reader;
public ProductService(ProductFileReader reader) {
this.reader = reader;
}
public List<Product> findAll(boolean inStockOnly) {
List<Product> selected = new ArrayList<>();
for (Product product : reader.getProducts()) {
if (!inStockOnly || product.getStock() > 0) {
selected.add(product);
}
}
return selected;
}
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/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;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping({"/", "/products"})
public String listProducts(
@RequestParam(name = "inStockOnly", defaultValue = "false") boolean inStockOnly,
Model model) {
model.addAttribute("products", service.findAll(inStockOnly));
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";
}
}
Practice
Change the stock rule from greater than zero to at least 10. Predict the filtered count from products.txt before running.
References: Spring constructor injection, Spring Data MongoDB repositories.
Why use different component annotations?
@Component marks a general helper such as ProductFileReader. @Service identifies ProductService as the home of application operations and business rules. @Repository identifies the data-access responsibility introduced in Stage 10. @Controller identifies web request handlers. These stereotypes help discovery and communicate responsibility; choosing a name does not automatically write the method logic. A repository stereotype can also participate in persistence exception translation when the relevant Spring infrastructure is configured.
After Stage 9: the Spring words to recognize
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| @Service | ProductService | A component with the role of performing application operations. |
| Business rule | product.getStock() > 0 | Our Java condition defines which products are in stock. Spring does not invent the rule. |
| Separation of responsibilities | Controller handles HTTP; service selects products | The structure makes each class’s job easier to find. |
One small check: Point to the class, constructor or annotation that demonstrates each term before continuing.
Concepts used in this stage
Use the right column for a quick reminder, or open a concept above for its full explanation.