Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active March 26, 2025 18:27
Show Gist options
  • Save rponte/49330963d98672e78fb3bdc3d887e461 to your computer and use it in GitHub Desktop.
Save rponte/49330963d98672e78fb3bdc3d887e461 to your computer and use it in GitHub Desktop.
Designing fault-tolerant and idempotent APIs with HTTP requests mapped to database's transactions at 1:1 model
/**
* This Spring Boot controller was implemented as an example of a simple but robust idempotent REST API that
* leverages the ACID properties of a relational database.
*/
@RestController
public class CreateNewUserController {
@Autowired
private UserRepository repository;
@Autowired
private EventsRepository eventsRepository; // a table that works as a queue for background jobs
@Transactional(
// we can use a strictier isolation level to prevent race conditions
isolation = Isolation.SERIALIZABLE
)
@PostMapping("/api/users")
public ResponseEntity<?> create(@RequestBody @Valid NewUserRequest request) {
// if user already exists, then we simplify the client-side returning status 200-OK
if (repository.existsByEmail(request.getEmail())) {
return ResponseEntity.ok("User exists"); // http 200
});
// create the new user
User user = request.toModel();
repository.save(user); // don't worry, we have an UNIQUE constraint here
// enqueue a job to tell an external service that a new user's been created
eventsRepository.save(
new Event("user_created:send_welcome_email", Map.of("email", user.getEmail()))
);
// pass back a successful response
URI location = ...;
return ResponseEntity
.created(location).build(); // http 201
}
/**
* Controller Advice for the UNIQUE constraint error
*/
@ExceptionHandler(ConstraintViolationException.class) // org.hibernate.exception.ConstraintViolationException
public ResponseEntity<?> handleUniqueConstraintErrors(ConstraintViolationException e, WebRequest request) {
Map<String, Object> body = Map.of(
"timestamp", LocalDateTime.now(),
"message", "User exists"
);
return ResponseEntity.ok(body); // http 200
}
/**
* Controller Advice for Serializable isolation level errors
*/
@ExceptionHandler({
CannotAcquireLockException.class, // org.springframework.dao.CannotAcquireLockException
LockAcquisitionException.class, // org.hibernate.exception.LockAcquisitionException (nested exception)
})
public ResponseEntity<?> handleSerializableErrors(RuntimeException e, WebRequest request) {
Map<String, Object> body = Map.of(
"timestamp", LocalDateTime.now(),
"message", "Sorry, another request is being processed at this moment"
);
return ResponseEntity
.status(HttpStatus.CONFLICT).body(body); // http 409
}
}
@rponte
Copy link
Author

rponte commented Mar 26, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment