Stage 11: Store and change products in MongoDB
Learning path / Stage 11 of 37
mvn spring-boot:run. Use the port specified for this stage and stop with Ctrl+C.Download spring-products-mongodb.zip · Complete file-by-file guide
Connect before running
Use a running local MongoDB server or your own Atlas cluster. The web application listens on 8089; MongoDB normally listens on 27017. These are different processes. The connection instructions below include local defaults and Atlas environment variables.
Map one product to one document
@Document("products") chooses the collection. @Id marks the product id as MongoDB’s identifier. These examples retain numeric IDs 1–30 and reserve 31 for the sample button. A collection holds product documents; a document contains the fields of one product.
Replace the repository implementation
ProductRepository now extends MongoRepository<Product, Integer>. Spring Data supplies a runtime implementation with methods such as findAll, findById, insert, save and deleteById. We remove FileProductRepository so there is one repository choice. Unlike Stage 10, we are using an interface specifically supported by Spring Data.
Seed a fresh demo database
ProductDataSeeder runs at startup through CommandLineRunner. If the products collection is empty, it copies the 30 file products into MongoDB. If data already exists, it leaves it alone. The file is now seed input; database reads supply the pages. Deleting every product and restarting will seed the collection again. Use a separate learning database.
Try create, read, update and delete
The list page has Create sample product (ID 31). Click it to insert a product and open its details. Change its stock to 0 and click Save stock. The controller validates non-negative stock and the service saves the change. Click Delete this product to remove it. Repeating Create returns the existing sample while it exists.
Understand the POST and redirect flow
Each write form sends POST. Java changes the database and returns redirect:/products or the details URL. The browser then requests that page with GET. Refreshing the resulting page therefore repeats a read. Unknown IDs produce 404; a negative submitted stock produces 400.
Prove persistence
Create ID 31, change its stock, stop the app and start it with the same database settings. The product and changed stock remain. This is persistence: the stored value survives the Java process ending. This small local learning app has no login or concurrent-edit protection; those are later topics.
Who implements findAll() and findById()?
In Stage 11, Spring Data MongoDB supplies the repository implementation. We declare the interface below; we do not write the MongoDB query code for these inherited methods.
public interface ProductRepository
extends MongoRepository<Product, Integer> {
}Product identifies the Java objects handled by the repository. Integer identifies their ID type. During startup, Spring Data creates a repository object implementing this interface. Spring supplies that object to ProductService through its constructor.
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}Our service can then call these methods on the supplied object:
repository.findAll(); // Read all products from MongoDB
repository.findById(1); // Find product ID 1; returns Optional<Product>Keep the two findAll methods separate: ProductService.findAll(boolean inStockOnly) is our own method. It calls repository.findAll() to read the stored products, then applies our Java stock-filtering rule. Spring Data supplies the repository method, not our business rule.
How does this relate to DAO?
DAO — Data Access Object names the responsibility of accessing stored data. A DAO exposes operations such as finding, saving and deleting records, keeping storage-specific code away from the service. It is an architectural term, not a Java keyword or a required class name.
| Stage | Contract | Object performing data access | Who supplies the implementation? |
|---|---|---|---|
| 10 | ProductRepository interface | FileProductRepository object | We write its method bodies. |
| 11 | ProductRepository extends MongoRepository | The repository object created by Spring Data | Spring Data supplies MongoDB access for the inherited operations. |
DAO emphasizes access to stored data. A repository presents operations on application objects, such as products. They are closely related ideas, with different emphasis. Our product repository fulfills the data-access responsibility you learned about as DAO. This example does not need an additional ProductDao layer wrapping the same operations.
Check your understanding: Who writes the stock filter? We do, in ProductService. Who implements the inherited repository findAll() and findById()? Spring Data MongoDB does. Does @Repository alone implement any ordinary Java interface? No; Stage 10 still needs our FileProductRepository method bodies.
MongoDB connection: choose one option
Local: start your installed MongoDB service/server on localhost:27017. The supplied properties already use that address and the dedicated database spring_stage11_products. No environment variables are needed for that default.
Atlas: create a cluster, a database user and an IP access-list entry for your current IP. Choose Connect → Drivers → Java and copy your cluster URI. Replace its username and password; percent-encode reserved characters in credentials. Use your own cluster hostname, not the placeholders below. See the complete Atlas setup guide.
Set the variables in the same terminal used to start Maven. Do not put real passwords in source code or commit them.
set "MONGODB_URI=mongodb+srv://YOUR_USER:YOUR_ENCODED_PASSWORD@YOUR_CLUSTER.mongodb.net/?retryWrites=true&w=majority"
set "MONGODB_DATABASE=spring_stage11_products"
mvn spring-boot:run$env:MONGODB_URI="mongodb+srv://YOUR_USER:YOUR_ENCODED_PASSWORD@YOUR_CLUSTER.mongodb.net/?retryWrites=true&w=majority"
$env:MONGODB_DATABASE="spring_stage11_products"
mvn spring-boot:runexport MONGODB_URI='mongodb+srv://YOUR_USER:YOUR_ENCODED_PASSWORD@YOUR_CLUSTER.mongodb.net/?retryWrites=true&w=majority'
export MONGODB_DATABASE='spring_stage11_products'
mvn spring-boot:runIf startup reports a connection timeout, check the server/cluster status, URI, database credentials, Atlas IP access list and network access. The application needs a reachable database to start successfully.
Run this stage
Use JDK 21+ and Maven 3.9+. Extract spring-products-mongodb.zip and open a terminal in the spring-products-mongodb folder containing pom.xml. Keep the source files in their supplied paths.
mvn spring-boot:runOpen http://localhost:8089/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=8089
# Local MongoDB is the default; set MONGODB_URI to use Atlas.
spring.mongodb.uri=${MONGODB_URI:mongodb://localhost:27017}
spring.mongodb.database=${MONGODB_DATABASE:spring_stage11_products}
Expected output and checks
Fresh database: 30 rows, 29 in stock. Create ID 31: 31 total. Set its stock to 0: still 31 total, 29 in stock. Restart: ID 31 still has stock 0. Delete ID 31: 30 total and its details URL 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/Product.java
package com.edupoly.products;
import java.math.BigDecimal;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document("products")
public class Product {
@Id
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/ProductRepository.java
package com.edupoly.products;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface ProductRepository extends MongoRepository<Product, Integer> {
}
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;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
public List<Product> findAll(boolean inStockOnly) {
List<Product> selected = new ArrayList<>();
for (Product product : repository.findAll()) {
if (!inStockOnly || product.getStock() > 0) {
selected.add(product);
}
}
return selected;
}
public Optional<Product> findById(int id) {
return repository.findById(id);
}
public Product createSample() {
Optional<Product> existing = repository.findById(31);
if (existing.isPresent()) {
return existing.get();
}
Product product = new Product();
product.setId(31);
product.setName("Practice Product");
product.setCategory("Study Essentials");
product.setPrice(new java.math.BigDecimal("250.00"));
product.setBrand("StudyWorks");
product.setStock(8);
product.setDescription("Created through the application's Create sample button.");
product.setWarrantyMonths(6);
return repository.insert(product);
}
public Product updateStock(Product product, int stock) {
product.setStock(stock);
return repository.save(product);
}
public void delete(int id) {
repository.deleteById(id);
}
}
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.PostMapping;
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";
}
@PostMapping("/products/sample")
public String createSample() {
Product product = service.createSample();
return "redirect:/products/" + product.getId();
}
@PostMapping("/products/{id}/stock")
public String updateStock(@PathVariable("id") int id,
@RequestParam("stock") int stock) {
if (stock < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Stock cannot be negative");
}
Product product = service.findById(id).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found"));
service.updateStock(product, stock);
return "redirect:/products/" + id;
}
@PostMapping("/products/{id}/delete")
public String delete(@PathVariable("id") int id) {
if (service.findById(id).isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
service.delete(id);
return "redirect:/products";
}
}
src/main/java/com/edupoly/products/ProductDataSeeder.java
package com.edupoly.products;
import java.util.Arrays;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class ProductDataSeeder implements CommandLineRunner {
private final ProductRepository repository;
private final ProductFileReader reader;
public ProductDataSeeder(ProductRepository repository, ProductFileReader reader) {
this.repository = repository;
this.reader = reader;
}
@Override
public void run(String... args) {
if (repository.count() == 0) {
repository.saveAll(Arrays.asList(reader.getProducts()));
System.out.println("Seeded 30 products into the empty demo database");
}
}
}
Practice
Create the sample, update stock to 0, restart and verify persistence, then delete the sample. Keep notes of the URL, HTTP method and database operation for each action.
References: Spring constructor injection, Spring Data MongoDB repositories.
After Stage 11: the Spring words to recognize
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| @Document and @Id | Product mapped to products; numeric id | Spring Data uses these annotations to map Java fields to MongoDB documents. |
| MongoRepository | ProductRepository extends MongoRepository<Product, Integer> | Spring Data supplies the data-access implementation for this supported interface. |
| CRUD | Insert, find, save, delete | Create, read, update and delete are operations on stored data. |
| Persistence | A stock update survives restart | MongoDB retains the data independently of this running web app. |
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.