Skip to main content

Why My Object Changes Don't Stick: In-Memory vs Database Persistence

Β· 9 min read
Mahmut Salman
Software Developer

"I changed the object's failedLoginAttempts field, but when I query the database again, it's back to the old value! In high school, changing object properties just worked - why do I need to call save() in Spring Boot?" This is the fundamental difference between in-memory objects (what you learned in school) and database-backed objects (what you use in production). Let's understand why object changes don't automatically persist to the database.

The Discovery​

You wrote this code:

if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
user.increaseFailedLoginAttempts(); // Change the object

if (user.getFailedLoginAttempts() >= 5) {
user.setAccountLocked(true);
userRepository.save(user); // Save to database
}
// ❌ If < 5 attempts, you DON'T save!

throw new InvalidCredentialsException("Invalid credentials");
}

The problem:

Login attempt 1: failedLoginAttempts = 1 (in memory, not saved)
Login attempt 2: Database still has 0, code reads 0, increments to 1 (in memory, not saved)
Login attempt 3: Database still has 0, code reads 0, increments to 1 (in memory, not saved)
...
Never reaches 5 because database never gets updated! 🚨

Your question:

"I discovered why this didn't work - because I don't save it to database. But during high school I was writing code and there was no DB, and this logic was working. I was creating objects and altering their values and I could reach their values. But in business world we have databases. I get it, but still I change the failedLoginAttempts property of the object. How come getFailedLoginAttempts() gets database value but not the values I assigned?"

Great question! Let's understand the two worlds: in-memory vs database.


High School Programming: In-Memory Objects​

How It Worked Without a Database​

public class Main {
public static void main(String[] args) {
// Create object in memory
User user = new User();
user.setUsername("testuser");
user.setFailedLoginAttempts(0);

System.out.println(user.getFailedLoginAttempts()); // 0

// Change the object
user.increaseFailedLoginAttempts();

System.out.println(user.getFailedLoginAttempts()); // βœ… 1

// Change again
user.increaseFailedLoginAttempts();

System.out.println(user.getFailedLoginAttempts()); // βœ… 2
}
}

Why it worked:

  • βœ… Object lives in RAM (Random Access Memory)
  • βœ… All changes stay in memory
  • βœ… Same object reference throughout the program
  • βœ… No database involved

Visual: In-Memory Only​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM (Computer Memory) β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ User object β”‚ β”‚
β”‚ β”‚ username: "testuser" β”‚ β”‚
β”‚ β”‚ failedLoginAttempts: 0 β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ ↓ user.increaseFailedLoginAttempts()
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ User object β”‚ β”‚
β”‚ β”‚ username: "testuser" β”‚ β”‚
β”‚ β”‚ failedLoginAttempts: 1 β”‚ β”‚ βœ… Changed in memory
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When program ends β†’ Memory cleared β†’ Object gone!

Limitation: When the program stops, all data is lost.


Business World: Database-Backed Objects​

The Two Worlds Problem​

@Service
public class AuthService {

@Autowired
private UserRepository userRepository;

public void login(LoginRequest request) {
// Step 1: Get from database
User user = userRepository.findByEmail("test@example.com");
System.out.println(user.getFailedLoginAttempts()); // 0 (from database)

// Step 2: Change in memory
user.increaseFailedLoginAttempts();
System.out.println(user.getFailedLoginAttempts()); // βœ… 1 (in memory)

// Step 3: DON'T save to database (missing userRepository.save(user))

// Step 4: Method ends, object discarded
}

public void loginAgain(LoginRequest request) {
// Step 5: Query database again (new request)
User freshUser = userRepository.findByEmail("test@example.com");
System.out.println(freshUser.getFailedLoginAttempts()); // ❌ 0 (database still has old value!)
}
}

Visual: Two Separate Worlds​

Request 1:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM (Temporary) β”‚
β”‚ β”‚
β”‚ User object (in memory) β”‚
β”‚ failedLoginAttempts: 0 β†’ 1 β”‚ βœ… Changed
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
❌ NOT SAVED
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Database (Persistent) β”‚
β”‚ β”‚
β”‚ users table β”‚
β”‚ failedLoginAttempts: 0 β”‚ ❌ Still old value
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request 1 ends β†’ RAM cleared β†’ Object discarded

Request 2:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM (Fresh start) β”‚
β”‚ β”‚
β”‚ User object (freshly loaded from DB) β”‚
β”‚ failedLoginAttempts: 0 β”‚ ❌ Back to 0!
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↑
Loaded from database
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Database (Persistent) β”‚
β”‚ β”‚
β”‚ users table β”‚
β”‚ failedLoginAttempts: 0 β”‚ ❌ Still 0
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key insight: Database is the source of truth. In-memory changes are temporary.


Why They Don't Sync Automatically​

The Lifecycle of a Database-Backed Object​

// Step 1: Load from database
User user = userRepository.findByEmail("test@example.com");
// JPA executes: SELECT * FROM users WHERE email = 'test@example.com'
// Creates Java object from database row

// Step 2: Object exists in memory
// user.failedLoginAttempts = 0 (copied from database)

// Step 3: Change in memory
user.increaseFailedLoginAttempts();
// user.failedLoginAttempts = 1 (only in RAM!)
// Database STILL has: failedLoginAttempts = 0

// Step 4: Save to database
userRepository.save(user);
// JPA executes: UPDATE users SET failed_login_attempts = 1 WHERE email = 'test@example.com'
// NOW database is updated!

// Step 5: Both in sync
// Memory: failedLoginAttempts = 1 βœ…
// Database: failed_login_attempts = 1 βœ…

Timeline Diagram​

Time β†’

T1: Load from DB
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Database β”‚ failedLoginAttempts = 0
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓ SELECT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM β”‚ failedLoginAttempts = 0
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

T2: Modify in memory
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Database β”‚ failedLoginAttempts = 0 (unchanged)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM β”‚ failedLoginAttempts = 1 (changed)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↕️ OUT OF SYNC!

T3: Save to database
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RAM β”‚ failedLoginAttempts = 1
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓ UPDATE
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Database β”‚ failedLoginAttempts = 1 (now updated)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
βœ… IN SYNC!

Your Specific Problem: Failed Login Attempts​

The Broken Code​

@PostMapping("/auth/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new UserNotFoundException("User not found"));

if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
// Change in memory
user.increaseFailedLoginAttempts();

// Only save if >= 5
if (user.getFailedLoginAttempts() >= 5) {
user.setAccountLocked(true);
userRepository.save(user); // βœ… Saved
}
// ❌ If < 5, NOT saved!

throw new InvalidCredentialsException("Invalid credentials");
}

// Successful login
user.setFailedLoginAttempts(0);
userRepository.save(user); // βœ… Saved
return ResponseEntity.ok(new LoginResponse(...));
}

What Actually Happens​

Attempt 1:

1. Load user from DB: failedLoginAttempts = 0
2. Password wrong: increaseFailedLoginAttempts() β†’ now 1 (in memory)
3. Check: 1 >= 5? No
4. DON'T save ❌
5. Request ends β†’ memory cleared
6. Database still has: failedLoginAttempts = 0

Attempt 2:

1. Load user from DB: failedLoginAttempts = 0 (database never changed!)
2. Password wrong: increaseFailedLoginAttempts() β†’ now 1 (in memory)
3. Check: 1 >= 5? No
4. DON'T save ❌
5. Request ends β†’ memory cleared
6. Database still has: failedLoginAttempts = 0

Attempt 3, 4, 5, 6, 7...

Same thing! Database NEVER gets updated because < 5.
Account NEVER locks because database always has 0!

The Fix​

@PostMapping("/auth/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new UserNotFoundException("User not found"));

if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
// Change in memory
user.increaseFailedLoginAttempts();

// Lock if >= 5
if (user.getFailedLoginAttempts() >= 5) {
user.setAccountLocked(true);
}

// βœ… ALWAYS save, regardless of count
userRepository.save(user);

throw new InvalidCredentialsException("Invalid credentials");
}

// Successful login
user.setFailedLoginAttempts(0);
userRepository.save(user); // βœ… Saved
return ResponseEntity.ok(new LoginResponse(...));
}

Now it works:

Attempt 1:

1. Load: failedLoginAttempts = 0
2. Increase: failedLoginAttempts = 1 (memory)
3. Save: Database updated to 1 βœ…

Attempt 2:

1. Load: failedLoginAttempts = 1 (from database)
2. Increase: failedLoginAttempts = 2 (memory)
3. Save: Database updated to 2 βœ…

Attempt 5:

1. Load: failedLoginAttempts = 4 (from database)
2. Increase: failedLoginAttempts = 5 (memory)
3. Lock: accountLocked = true
4. Save: Database updated: failedLoginAttempts = 5, accountLocked = true βœ…

Mental Model: Two Separate Worlds​

High School: One World (Memory Only)​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Your Program β”‚
β”‚ β”‚
β”‚ User user = new User(); β”‚
β”‚ user.setFailedLoginAttempts(0); β”‚
β”‚ β”‚
β”‚ user.increaseFailedLoginAttempts(); β”‚
β”‚ // failedLoginAttempts = 1 βœ… β”‚
β”‚ β”‚
β”‚ System.out.println( β”‚
β”‚ user.getFailedLoginAttempts() β”‚
β”‚ ); // Prints 1 βœ… β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Everything happens in memory.
Changes are immediate and visible.
No persistence needed.

Business World: Two Worlds (Memory + Database)​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ World 1: RAM (Temporary) β”‚
β”‚ β”‚
β”‚ User user = repo.find(...); β”‚
β”‚ user.setFailedLoginAttempts(0); β”‚
β”‚ user.increaseFailedLoginAttempts(); β”‚
β”‚ // failedLoginAttempts = 1 in memory βœ… β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↕️
Must explicitly sync
↕️
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ World 2: Database (Persistent) β”‚
β”‚ β”‚
β”‚ users table: β”‚
β”‚ failed_login_attempts = 0 ❌ β”‚
β”‚ β”‚
β”‚ (Database doesn't change automatically) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

↓ userRepository.save(user)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ World 2: Database (Persistent) β”‚
β”‚ β”‚
β”‚ users table: β”‚
β”‚ failed_login_attempts = 1 βœ… β”‚
β”‚ β”‚
β”‚ (NOW updated) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why Database is the Source of Truth​

Scenario: Multiple Requests​

// Request 1 (from browser)
User user1 = userRepository.findByEmail("test@example.com");
user1.setFailedLoginAttempts(3);
// Don't save

// Request 2 (from mobile app, same user)
User user2 = userRepository.findByEmail("test@example.com");
System.out.println(user2.getFailedLoginAttempts());
// ❌ Prints 0, not 3!
// Because database still has 0

Key insight: Each request creates a new Java object from the database.

Request 1:
Database (failedLoginAttempts = 0)
↓ SELECT
Object 1 (failedLoginAttempts = 0)
↓ user1.setFailedLoginAttempts(3)
Object 1 (failedLoginAttempts = 3)
❌ NOT SAVED
Request ends β†’ Object 1 discarded

Request 2:
Database (failedLoginAttempts = 0) ← Still 0!
↓ SELECT
Object 2 (failedLoginAttempts = 0) ← Fresh from DB

Objects are independent copies! Changing one doesn't affect the other.


App Restart: In-Memory Changes Lost​

Without Save​

// App running
User user = userRepository.findByEmail("test@example.com");
user.setFailedLoginAttempts(3);
// Don't save

// App restarts
// All RAM cleared!
// Object gone!

// App starts again
User freshUser = userRepository.findByEmail("test@example.com");
System.out.println(freshUser.getFailedLoginAttempts());
// ❌ Prints 0 (database still has 0)

With Save​

// App running
User user = userRepository.findByEmail("test@example.com");
user.setFailedLoginAttempts(3);
userRepository.save(user); // βœ… Persisted to database

// App restarts
// RAM cleared, but database survives!

// App starts again
User freshUser = userRepository.findByEmail("test@example.com");
System.out.println(freshUser.getFailedLoginAttempts());
// βœ… Prints 3 (loaded from database)

Complete Comparison​

High School (In-Memory Only)​

public class InMemoryExample {
public static void main(String[] args) {
User user = new User();
user.setUsername("testuser");
user.setFailedLoginAttempts(0);

// Changes are immediate
user.increaseFailedLoginAttempts();
System.out.println(user.getFailedLoginAttempts()); // βœ… 1

user.increaseFailedLoginAttempts();
System.out.println(user.getFailedLoginAttempts()); // βœ… 2

// But when program ends...
// ALL DATA LOST! 🚨
}
}

Characteristics:

  • βœ… Changes are immediate
  • βœ… Same object throughout execution
  • ❌ Data lost when program ends
  • ❌ Can't share data between program runs
  • ❌ Can't handle concurrent requests

Business World (Database-Backed)​

@Service
public class DatabaseExample {

@Autowired
private UserRepository userRepository;

public void example() {
// Load from database
User user = userRepository.findByEmail("test@example.com");
user.setFailedLoginAttempts(0);

// Change in memory
user.increaseFailedLoginAttempts();
System.out.println(user.getFailedLoginAttempts()); // βœ… 1 (in memory)

// Save to database
userRepository.save(user); // βœ… Persists to database

// Even if app restarts, data survives!
// Even if another user logs in simultaneously, they see correct data!
}
}

Characteristics:

  • βœ… Data survives app restarts
  • βœ… Multiple users can access same data
  • βœ… Concurrent requests handled correctly
  • ⚠️ Must explicitly save changes
  • ⚠️ Each request gets fresh copy from database

Common Mistakes​

Mistake 1: Forgetting to Save​

// ❌ WRONG
User user = userRepository.findByEmail(email);
user.setFailedLoginAttempts(3);
// Forgot userRepository.save(user)
// Database NEVER updated!

Mistake 2: Saving Conditionally​

// ❌ WRONG
User user = userRepository.findByEmail(email);
user.increaseFailedLoginAttempts();

if (user.getFailedLoginAttempts() >= 5) {
userRepository.save(user); // Only saves when >= 5
}
// Database never updated for attempts 1-4!

Mistake 3: Assuming Changes Persist​

// ❌ WRONG
public void loginAttempt1() {
User user = userRepository.findByEmail(email);
user.increaseFailedLoginAttempts();
// Don't save
}

public void loginAttempt2() {
User user = userRepository.findByEmail(email);
// Assuming failedLoginAttempts = 1 from previous attempt
// ❌ WRONG! It's 0 again because we never saved!
}

Best Practices​

βœ… DO: Always Save After Modifying​

User user = userRepository.findByEmail(email);
user.increaseFailedLoginAttempts();
userRepository.save(user); // βœ… Always save

βœ… DO: Save After Any State Change​

if (!passwordEncoder.matches(password, user.getPassword())) {
user.increaseFailedLoginAttempts();

if (user.getFailedLoginAttempts() >= 5) {
user.setAccountLocked(true);
}

userRepository.save(user); // βœ… Save ALL changes
}

βœ… DO: Reset and Save on Success​

if (passwordEncoder.matches(password, user.getPassword())) {
user.setFailedLoginAttempts(0);
user.setLastLoginAt(LocalDateTime.now());
userRepository.save(user); // βœ… Save successful login
}

❌ DON'T: Assume In-Memory Changes Persist​

// ❌ WRONG
User user = userRepository.findByEmail(email);
user.setFailedLoginAttempts(3);
// Assumption: "It's changed, so database should have it"
// Reality: Database still has old value!

Summary​

High School vs Business World​

AspectHigh School (In-Memory)Business World (Database)
Object lifetimeUntil program endsUntil method ends
Data persistenceLost when program endsSurvives app restarts
ChangesImmediate and visibleTemporary unless saved
Multiple requestsN/A (single execution)Each request gets fresh copy
SynchronizationNot neededMust call save()
Source of truthThe object itselfThe database

Key Takeaways​

  1. In-memory changes are temporary - they only exist in RAM
  2. Database is the source of truth - each query loads fresh data from database
  3. Must explicitly save - userRepository.save(user) to persist changes
  4. Each request is independent - objects from different requests don't share memory
  5. App restart clears memory - only database data survives

The Mental Model​

High School:
Object in RAM β†’ Change it β†’ βœ… Done!

Business World:
Object in RAM β†’ Change it β†’ Save to DB β†’ βœ… Done!
↑
Must call save()!

Your Specific Problem: The Fix​

// ❌ BEFORE (broken)
user.increaseFailedLoginAttempts();
if (user.getFailedLoginAttempts() >= 5) {
userRepository.save(user); // Only saves at 5
}

// βœ… AFTER (fixed)
user.increaseFailedLoginAttempts();
if (user.getFailedLoginAttempts() >= 5) {
user.setAccountLocked(true);
}
userRepository.save(user); // ALWAYS save

Now you understand why database-backed objects need explicit save() calls - because memory and database are two separate worlds that don't automatically synchronize! 🎯