Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active January 9, 2024 14:37
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 Dec 29, 2023

An important consideration is that the process that combines recording the idempotent token and all mutating operations related to servicing the request must meet the properties for an atomic, consistent, isolated, and durable (ACID) operation. An ACID server-side operation needs to be an “all or nothing” or atomic process. This ensures that we avoid situations where we could potentially record the idempotent token and fail to create some resources or, alternatively, create the resources and fail to record the idempotent token.

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