STAGE 36 OF 37

Stage 36: join a transaction with REQUIRED

Learning path / Stage 36 of 37

Before you start: use JDK 21+ and Maven 3.9+. Extract this stage’s project and open a terminal in its pom.xml folder. Complete the setup below, including a database connection where required, then follow the commands in this lesson’s Run this example section. Console lessons finish after printing their result; web lessons keep running until Ctrl+C. Follow the lesson’s run section for profile and database settings.

Run a controlled failure and inspect the committed MongoDB documents instead of guessing what the annotation does.

Download the complete Stage 36 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.

Lesson → Transactional service proxy → Todo insert → Activity / audit insert → Commit or rollback → Inspect collections

What we change and why

  1. Use a concrete consistency requirement

    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.

  2. Configure the transaction infrastructure

    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.

  3. Prepare collections before the transaction

    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.

  4. Call through an injected service

    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.

  5. Cross a real service boundary

    TodoService receives ActivityService through its constructor. Calling activity.record passes through that bean’s transaction proxy. REQUIRED joins the existing transaction; if none exists it starts one. It does not make an independent commit.

  6. Verify the outer failure

    The activity insert and todo insert both roll back when the unchecked outer failure exits the outer transaction. A caught inner failure may mark a shared transaction rollback-only and lead to UnexpectedRollbackException at outer completion; that is not an independent rollback.

MongoDB connection before running

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.

Windows Command Prompt
set "LESSON_MONGODB_URI=mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_36?retryWrites=true&w=majority"
Windows PowerShell
$env:LESSON_MONGODB_URI="mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_36?retryWrites=true&w=majority"
macOS / Linux
export LESSON_MONGODB_URI="mongodb+srv://USERNAME:PASSWORD@YOUR-CLUSTER.mongodb.net/spring_stage_36?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.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · Windows, macOS and Linux
mvn test -DmongoIntegration=true
mvn spring-boot:run

This 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

Expected output · ignore framework startup messages
Outer failure: todos=0, activity=0, audit=0
Outer success: todos=1, activity=1, 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.

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
pom.xml · complete file
<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-36-required</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>
src/main/java/com/edupoly/lesson/AppConfig.java
src/main/java/com/edupoly/lesson/AppConfig.java · complete file
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);
    }
}
src/main/java/com/edupoly/lesson/LearningData.java
src/main/java/com/edupoly/lesson/LearningData.java · complete file
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"));
    }
}
src/main/java/com/edupoly/lesson/ActivityService.java
src/main/java/com/edupoly/lesson/ActivityService.java · complete file
package com.edupoly.lesson;

import org.bson.Document;
import org.springframework.stereotype.Service;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.transaction.annotation.*;
@Service public class ActivityService {
    private final MongoTemplate template;
    public ActivityService(MongoTemplate template) { this.template = template; }
    @Transactional(propagation = Propagation.REQUIRED) public void record() {
        template.insert(new Document("action", "Todo created"), "lesson_activity");
    }
}
src/main/java/com/edupoly/lesson/TodoService.java
src/main/java/com/edupoly/lesson/TodoService.java · complete file
package com.edupoly.lesson;

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; private final ActivityService activity;
    public TodoService(MongoTemplate template, ActivityService activity) {
        this.template = template; this.activity = activity;
    }
    @Transactional public void create(boolean fail) {
        template.insert(new Document("title", "Learn propagation"), "lesson_todos");
        activity.record();
        if (fail) throw new IllegalStateException("Intentional outer failure");
    }
}
src/main/java/com/edupoly/lesson/Lesson.java
src/main/java/com/edupoly/lesson/Lesson.java · complete file
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.create(true); } catch (IllegalStateException ex) { System.out.println(ex.getMessage()); }
        data.print("Outer failure"); data.clear();
        service.create(false); data.print("Outer success");
    }
}
src/main/java/com/edupoly/lesson/Application.java
src/main/java/com/edupoly/lesson/Application.java · complete file
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/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
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.create(true));
            assertEquals(0, data.count("lesson_todos")); assertEquals(0, data.count("lesson_activity"));
            assertEquals(0, data.count("lesson_audit")); data.clear();
            service.create(false); assertEquals(1, data.count("lesson_todos")); assertEquals(1, data.count("lesson_activity"));
            } finally { data.clear(); }
        }
    }
}
src/main/resources/application.properties
src/main/resources/application.properties · complete file
spring.main.web-application-type=none

Try a failure, then correct it

Move the annotated inner operation into the same class and call it through this. Its own propagation annotation is bypassed. Restore the separate injected service.

Words you can now explain

WordMeaning in this example
REQUIREDParticipates in an existing transaction or starts one if absent.

Practice assignment

Predict every final collection count before running. Then add a second successful todo creation and explain which transaction owns each insert.

Read the official documentation →

Concepts used in this stage

Use the right column for a quick reminder, or open a concept above for its full explanation.

Java 21 · Spring Boot 4.1.1 · Edupoly