Run a controlled failure and inspect the committed MongoDB documents instead of guessing what the annotation does.
Open this stage in the online learning path
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.
Creating a todo and its activity record is one business operation. A partial result is confusing: the todo exists but the activity record does not. An individual MongoDB document write is already atomic; this example needs a transaction because multiple documents must agree.
AppConfig creates the client, MongoTemplate, database factory and MongoTransactionManager. @EnableTransactionManagement enables annotation-driven transaction proxies. All template operations use the same factory so they participate in the transaction-bound session.
LearningData creates lesson-owned collections before the service is called. It clears those collections only in the chosen disposable learning database, outside transactional methods. Never point this teaching project at a database containing records you want to keep.
Lesson receives TodoService from Spring. The transaction interceptor starts the transaction before entering an annotated method, then commits or rolls back after the method exits. This is a service proxy, not a proxy created for the database itself.
The service does not swallow the failure. Lesson catches it only after the proxied call returns exceptionally. Catching an exception inside the method and returning normally can prevent the transaction interceptor from seeing that failure.
With this default configuration RuntimeException and Error trigger rollback. A checked IOException does not: the insert commits even though the caller receives an exception. The example uses IOException to show the rule; it does not pretend to perform an external file operation.
rollbackFor=IOException.class requests rollback for that checked exception. Spring also permits global customization of rollback defaults; this lesson deliberately uses the ordinary defaults to make the comparison visible.
Use a disposable learning database on an Atlas replica-set cluster. Follow the existing Atlas setup chapter to create a database user and allow your IP. Use a fresh database name for this lesson. The code inserts only demonstration records, clears only its lesson-owned collections, and prints the final counts.
Set LESSON_MONGODB_URI in the same terminal where Maven will run. Replace the example credentials and cluster host; percent-encode special characters in the password. Keep the URI out of source control.
set "LESSON_MONGODB_URI=mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_35?retryWrites=true&w=majority"$env:LESSON_MONGODB_URI="mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_35?retryWrites=true&w=majority"export LESSON_MONGODB_URI="mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_35?retryWrites=true&w=majority"A standalone local mongod does not support these multi-document transactions. Use Atlas or an initialized local replica set. The database name in the URI is required by this example. The project explicitly registers MongoTransactionManager; the annotation alone does not enable MongoDB transactions. Collections are created before starting a transaction.
Open a terminal in the extracted folder containing pom.xml. Run one command at a time.
mvn test -DmongoIntegration=true
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.
Runtime default: todos=0, activity=0, audit=0
Checked default: todos=1, activity=0, audit=0
Checked with rollbackFor: todos=0, activity=0, audit=0
Intentional failure messages also appear between these summaries.Object identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.
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.
<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-35-rollback-rules</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-data-mongodb</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
package com.edupoly.lesson;
import com.mongodb.ConnectionString;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.springframework.context.annotation.*;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration @ComponentScan("com.edupoly.lesson") @EnableTransactionManagement
public class AppConfig {
private ConnectionString connection() {
String uri = System.getenv("LESSON_MONGODB_URI");
if (uri == null || uri.isBlank()) throw new IllegalStateException("Set LESSON_MONGODB_URI to a disposable replica-set database URI first");
var connection = new ConnectionString(uri);
if (connection.getDatabase() == null) throw new IllegalStateException("Include a learning database name in LESSON_MONGODB_URI");
return connection;
}
@Bean(destroyMethod = "close") public MongoClient mongoClient() { return MongoClients.create(connection()); }
@Bean public MongoTemplate mongoTemplate(MongoClient client) { return new MongoTemplate(client, connection().getDatabase()); }
@Bean public MongoDatabaseFactory databaseFactory(MongoTemplate template) { return template.getMongoDatabaseFactory(); }
@Bean public MongoTransactionManager transactionManager(MongoDatabaseFactory databaseFactory) {
return new MongoTransactionManager(databaseFactory);
}
}
package com.edupoly.lesson;
import org.springframework.stereotype.Component;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
@Component public class LearningData {
private final MongoTemplate template;
public LearningData(MongoTemplate template) { this.template = template; }
public void prepare() {
for (String collection : new String[]{"lesson_todos", "lesson_activity", "lesson_audit"}) {
if (!template.collectionExists(collection)) template.createCollection(collection);
}
}
public void clear() {
for (String collection : new String[]{"lesson_todos", "lesson_activity", "lesson_audit"}) {
template.remove(new Query(), collection);
}
}
public long count(String collection) { return template.count(new Query(), collection); }
public void print(String label) {
System.out.println(label + ": todos=" + count("lesson_todos") + ", activity=" + count("lesson_activity") + ", audit=" + count("lesson_audit"));
}
}
package com.edupoly.lesson;
import java.io.IOException;
import org.bson.Document;
import org.springframework.stereotype.Service;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.transaction.annotation.Transactional;
@Service public class TodoService {
private final MongoTemplate template;
public TodoService(MongoTemplate template) { this.template = template; }
@Transactional public void runtimeFailure() {
template.insert(new Document("title", "Runtime example"), "lesson_todos");
throw new IllegalStateException("Unchecked failure");
}
@Transactional public void checkedFailure() throws IOException {
template.insert(new Document("title", "Checked example"), "lesson_todos");
throw new IOException("Checked failure");
}
@Transactional(rollbackFor = IOException.class) public void checkedRollback() throws IOException {
template.insert(new Document("title", "Explicit rollback example"), "lesson_todos");
throw new IOException("Checked failure with rollbackFor");
}
}
package com.edupoly.lesson;
import org.springframework.stereotype.Component;
@Component public class Lesson {
private final TodoService service; private final LearningData data;
public Lesson(TodoService service, LearningData data) { this.service = service; this.data = data; }
public void run() throws Exception {
data.prepare();
data.clear();
try { service.runtimeFailure(); } catch (IllegalStateException ex) { System.out.println(ex.getMessage()); }
data.print("Runtime default"); data.clear();
try { service.checkedFailure(); } catch (java.io.IOException ex) { System.out.println(ex.getMessage()); }
data.print("Checked default"); data.clear();
try { service.checkedRollback(); } catch (java.io.IOException ex) { System.out.println(ex.getMessage()); }
data.print("Checked with rollbackFor");
}
}
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();
}
}
}
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
@org.junit.jupiter.api.condition.EnabledIfSystemProperty(named = "mongoIntegration", matches = "true")
void actualMongoTransactionsHaveExpectedOutcome() {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
var data = context.getBean(LearningData.class); var service = context.getBean(TodoService.class);
data.prepare(); data.clear();
try {
assertThrows(IllegalStateException.class, () -> service.runtimeFailure()); assertEquals(0, data.count("lesson_todos"));
assertThrows(java.io.IOException.class, () -> service.checkedFailure()); assertEquals(1, data.count("lesson_todos")); data.clear();
assertThrows(java.io.IOException.class, () -> service.checkedRollback()); assertEquals(0, data.count("lesson_todos"));
} finally { data.clear(); }
}
}
}
spring.main.web-application-type=none
Remove rollbackFor from checkedRollback. The checked-exception insert now commits. Restore the rule.
| Word | Meaning in this example |
|---|---|
| Rollback rule | Determines whether an escaping exception requests rollback. |
| rollbackFor | Explicitly includes a checked exception type. |
Predict every final collection count before running. Then add a second successful todo creation and explain which transaction owns each insert.