eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Regression testing is an important step in the release process, to ensure that new code doesn't break the existing functionality. As the codebase evolves, we want to run these tests frequently to help catch any issues early on.

The best way to ensure these tests run frequently on an automated basis is, of course, to include them in the CI/CD pipeline. This way, the regression tests will execute automatically whenever we commit code to the repository.

In this tutorial, we'll see how to create regression tests using Selenium, and then include them in our pipeline using GitHub Actions:, to be run on the LambdaTest cloud grid:

>> How to Run Selenium Regression Tests With GitHub Actions

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Overview

When we use Spring Data JPA, calling the repository.save(entity) method may seem deceptively simple. Many of us tend to continue using the original entity instance after saving, assuming it accurately represents the persisted data. However, this assumption is not always correct.

In this tutorial, we explain why it is important to always use the instance returned by the save() method, using specific unit tests to show what happens during both persisting and merging scenarios. By the end, we will clearly illustrate how ignoring the returned value can cause subtle and difficult-to-diagnose bugs.

2. Introduction to the Problem

The CrudRepository.save() method in Spring Data JPA has the following signature:

<S extends T> S save(S entity);

This signature already implies that save() returns an entity instance, but it doesn’t guarantee that the instance is the same one we passed in.

Further, the JavaDoc of this method suggests:

Saves a given entity. Use the returned instance for further operations as the save operation might have changed the entity instance completely.

However, when working with Spring Data JPA, it is very common to see code like this:

repository.save(article);
// ... continue using the article object

At first glance, this looks perfectly reasonable. However, this approach can lead to subtle bugs and misunderstandings. Thus, sometimes this works, and sometimes it does not.

To understand why, we need to distinguish between two fundamentally different scenarios: persisting a new entity and merging an entity.

As usual, we’ll address it through examples. We’ll create a simple Spring Boot application and use unit tests to demonstrate it.

So next, let’s prepare an entity class:

@Entity
@Table(name = "baeldung_articles")
public class BaeldungArticle {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String content;

    private String author;

    // getters and setters are omitted
}

Then, let’s create a BaeldungArticleRepo interface to extend JpaRepository:

@Repository
interface BaeldungArticleRepo extends JpaRepository<BaeldungArticle, Long> {
}

For simplicity, we’ll use an in-memory H2 database as the datastore for our simple Spring Boot application. However, we’ll skip the datasource configuration in this tutorial.

Since we’ll use unit tests to demonstrate the behaviors of the save() method, let’s create a unit test class:

//... Spring Boot test related annotations omitted
public class ReturnedValueOfSaveIntegrationTest {
    @Autowired
    private BaeldungArticleRepo repo;

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}

As we can see, we injected BaeldungArticleRepo and the entityManger into this test class, which we’ll use in later test methods.

Next, let’s create test methods to demonstrate save()’s behaviors in two scenarios.

3. The Persisting Scenario

Let’s start with the simplest case: saving a brand-new entity:

@Test
void whenNewArticleIsSaved_thenOriginalAndSavedResultsAreTheSame() {
    BaeldungArticle article = new BaeldungArticle();
    article.setTitle("Learning about Spring Data JPA");
    article.setContent(" ... the content ...");
    article.setAuthor("Kai Yuan");
 
    assertNull(article.getId());
 
    BaeldungArticle savedArticle = repo.save(article);
    assertNotNull(article.getId());
 
    assertSame(article, savedArticle);
}

In this example, we first create a new BaeldungArticle instance (article) and set its properties. However, we don’t assign an ID to itAfter calling repo.save(article), JPA persists it in the database and returns the persisted entity.

Additionally, the assertSame() assertion demonstrates that when we create a new object, article, and call repo.save(article), the returned object, savedArticle, and article are actually the same object.

To understand why it works like this, let’s quickly look at the save() method implementation in Spring Data JPA’s SimpleJpaRepository class:

@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
        this.entityManager.persist(entity);
        return entity;
    } else {
        return (S)this.entityManager.merge(entity);
    }
}

As we observe, passing a new entity to the save() method results in calling EntityManager.persist(entity), which persists the entity and returns the same entity object we passed to it.

In our test, we did not assign an ID to the article object. Therefore, JPA considers it a new entity and internally calls EntityManager.persist(). As a result, the original object and the returned object are the same reference.

This behavior often leads us to believe that reassigning the result of save() is unnecessary. However, this assumption fails once we encounter a merging scenario.

4. The Merging Scenario

Now, let’s examine what happens when we save an entity that already has an ID.

Let’s first look at a new test method:

@Test
@Transactional
void whenArticleIsMerged_thenOriginalAndSavedResultsAreNotTheSame() {
    // prepare an existing theArticle
    BaeldungArticle theArticle = new BaeldungArticle();
    theArticle.setTitle("Learning about Spring Boot");
    theArticle.setContent(" ... the content ...");
    theArticle.setAuthor("Kai Yuan");
    BaeldungArticle existingOne = repo.save(theArticle);
    Long id = existingOne.getId();

    // create a detached theArticle with the same id
    BaeldungArticle articleWithId = new BaeldungArticle();
    articleWithId.setTitle("Learning Kotlin");
    articleWithId.setContent(" ... the content ...");
    articleWithId.setAuthor("Eric");
    articleWithId.setId(id); //set the same id

    BaeldungArticle savedArticle = repo.save(articleWithId);
    assertEquals("Learning Kotlin", savedArticle.getTitle());
    assertEquals("Eric", savedArticle.getAuthor());
    assertEquals(id, savedArticle.getId());

    assertNotSame(articleWithId, savedArticle);
    assertFalse(entityManager.contains(articleWithId));
    assertTrue(entityManager.contains(savedArticle));
}

Let’s first quickly understand why we need the @Transactional annotation on this test method. This is because we want to check if an entity is managed by JPA in this test method. The default PersistenceContextType for @PersistenceContext is TRANSACTION, meaning we use a transaction-scoped persistence context.

As we have seen, the save() implementation has the @Transactional annotation. Therefore, if we don’t annotate our test method with @Transactional, the persistence context is closed after each save() call. Thus, outside a transaction scope, all entities are detached.

If we give the test a run, it passes. Next, let’s have a look at what the test does and what it tells us.

First, we persisted a new BaeldungArticle entity (theArticle) by calling the repo.save() method and obtained the ID generated by the H2 database.

Then, we created a new BaeldungArticle object named articleWithId, assigned the ID of the previously persisted entity to articleWithId.idand set different values in the other fields.

Subsequently, we called repo.save(articleWithId) and assigned the returned value to savedArticle. At this point, JPA recognized articleWithId as a detached entity. Internally, JPA uses EntityManager.merge() to first create a new managed instance and then merge the detached instance’s state into it. Finally, the managed entity is returned.

Therefore, articleWithId and savedArticle are not the same object, even though, as the assertions show, their fields, including their IDs, hold the same values.

Some of us may think, “ok, although they are not the same object, as long as their properties have the same values, there is no difference; we can use whichever one in further processing.”

The last two assertions with entityManager.contains() checks told us that articleWithId is a detached entity. On the other hand, savedArticle is a managed entity.

This is the critical reason why we must use the returned instance. Next, let’s understand why using a managed instance is essential.

5. Detached vs. Managed

There are many differences between detached and managed instances in JPA, for example:

  • Transaction participation – Managed instances participate in the current transaction, but detached instances don’t.
  • Automatic SQL execution – Managed instances automatically trigger INSERT/UPDATE/DELETE statements on flush. Detached instances don’t.
  • Lazy loading – Managed instances can initialize lazy associations. But detached instances can’t.
  • Cascade behavior -Managed instances participate in cascade persist/update operations while detached instances don’t.
  • Lifecycle callbacks – Managed instances trigger callbacks such as @PreUpdate, @PostUpdate, etc., but detached instances don’t.
  • More differences, such as refresh capability, rollback behavior, reattachment, identity guarantee, etc.

We won’t demonstrate each item on the list above in this tutorial. Instead, let’s take “lifecycle callbacks” as one example to show why it’s crucial to work with managed instances.

Let’s add a new @Transient field and a markAsSaved() method to our BaeldungArticle class:

@Entity
@Table(name = "baeldung_articles")
public class BaeldungArticle {
    //... existing fields omitted

    @Transient
    private boolean alreadySaved = false;

    @PostPersist
    @PostUpdate
    private void markAsSaved() {
        this.alreadySaved = true;
    }

    // ... getters and setters
}

We annotated markAsSaved() with @PostPersist and @PostUpdate to automatically set the alreadySaved flag when the entity is saved.

Now, let’s create a test to show why using the managed instance returned by save() in further processing is important:

@Test
void whenArticleIsMerged_thenDetachedObjectCanHaveDifferentValuesFromTheManagedOne() {
    // prepare an existing theArticle
    BaeldungArticle theArticle = new BaeldungArticle();
    theArticle.setTitle("Learning about Java Classes");
    theArticle.setContent(" ... the content ...");
    theArticle.setAuthor("Kai Yuan");
    BaeldungArticle existingOne = repo.save(theArticle);
    Long id = existingOne.getId();
 
    // create a detached theArticle with the same id
    BaeldungArticle articleWithId = new BaeldungArticle();
    theArticle.setTitle("Learning Kotlin Classes");
    theArticle.setContent(" ... the content ...");
    theArticle.setAuthor("Eric");
    articleWithId.setId(id); //set the same id
 
    BaeldungArticle savedArticle = repo.save(articleWithId);
    assertNotSame(articleWithId, savedArticle);
 
    assertFalse(articleWithId.isAlreadySaved());
    assertTrue(savedArticle.isAlreadySaved());
}

In this test, similarly, we first prepared an existing database entry and obtained its ID. Then we created a detached object, articleWithId, and assigned the ID to it.

After calling repo.save(articleWithId), the returned value savedArticle and articleWithId have different values in the alreadySaved field. This is because lifecycle callbacks (@PostPersist, @PostUpdate) are executed only on managed entities. The detached instance doesn’t receive these updates. 

At this point, continuing to use the detached instance is incorrect and dangerous.

6. Conclusion

In this article, we’ve examined the return value of the JPA repository’s save() method through examples. When we persist a new entity, the returned instance is often the same as the original. However, when we merge a detached entity, the returned instance is always a different object.

For this reason, we should always use the returned instance because only that instance is:

  • Managed by JPA
  • Tracked for changes
  • Updated by lifecycle callbacks
  • Synchronized with the database properly

Using the returned instance ensures correct behavior and avoids subtle persistence-related bugs.

As always, the complete source code for the examples is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LSD – NPI (cat=JPA)
announcement - icon

Get started with Spring Data JPA through the reference Learn Spring Data JPA:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments