Stage 21: observe a prototype captured by a singleton
Learning path / Stage 21 of 37
Understand why repeated service calls reuse one injected prototype.
Download the complete Stage 21 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.
This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.
What we change and why
Reuse the Stage 20 report draft
ReportDraft still has prototype scope. The new ReportService has default singleton scope.
Resolve the dependency once
Spring resolves ReportDraft while constructing the singleton service. The service then keeps that particular reference. Prototype scope has not changed; no new resolution happens in draftForOperation().
Compare two business operations
The two calls return the same reference.
Run this example
Open a terminal in the extracted folder containing pom.xml. Run one command at a time.
mvn test
mvn spring-boot:runThis is a console lesson. It does not open a web port. Application.main starts a Spring context, calls the demonstration and closes the context. Read the terminal, not a browser. Spring Boot’s Maven plugin is used as a convenient launcher; these examples show Spring Framework directly.
Expected result
Two operations share a draft: trueObject identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.
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-21-prototype-injection</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>
</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/ReportDraft.java
package com.edupoly.lesson;
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Scope;
@Component @Scope("prototype")
public class ReportDraft { }
src/main/java/com/edupoly/lesson/ReportService.java
package com.edupoly.lesson;
import org.springframework.stereotype.Service;
@Service public class ReportService {
private final ReportDraft draft;
public ReportService(ReportDraft draft) { this.draft = draft; }
public ReportDraft draftForOperation() { return draft; }
}
src/main/java/com/edupoly/lesson/Lesson.java
package com.edupoly.lesson;
import org.springframework.stereotype.Component;
@Component public class Lesson {
private final ReportService reports;
public Lesson(ReportService reports) { this.reports = reports; }
public void run() {
ReportDraft first = reports.draftForOperation();
ReportDraft second = reports.draftForOperation();
System.out.println("Two operations share a draft: " + (first == second));
}
}
src/main/java/com/edupoly/lesson/Application.java
package com.edupoly.lesson;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) throws Exception {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
context.getBean(Lesson.class).run();
}
}
}
src/main/java/com/edupoly/lesson/AppConfig.java
package com.edupoly.lesson;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.edupoly.lesson")
public class AppConfig { }
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 operationsHaveExpectedDraftIdentity() {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
var service = context.getBean(ReportService.class);
assertSame(service.draftForOperation(), service.draftForOperation());
}
}
}
Try a failure, then correct it
Change the service to create new ReportDraft() itself. The identity changes, but the draft bypasses Spring configuration and lifecycle processing. Restore container-based resolution.
Words you can now explain
| Word | Meaning in this example |
|---|---|
| Prototype dependency | A fresh object is produced whenever this bean is resolved. |
| Singleton service | One service is reused by all calls. |
| Dependency resolution | The constructor dependency is resolved when the singleton service is created. |
Practice assignment
Explain where the one prototype object is created and why a second service method call does not create another.
Concepts used in this stage
Use the right column for a quick reminder, or open a concept above for its full explanation.