January 20, 2025

Enforcing Unique Constraints on Email and Phone in MongoDB with Spring Boot WebFlux

 In this article, we’ll explore how to enforce uniqueness constraints on MongoDB fields with the following requirements:

  • email: Must always be unique and cannot be null.
  • phone: Optional but, if provided, must also be unique.

We'll explore different approaches to enforce these constraints using Spring Boot WebFlux in a reactive application.


Problem Setup

Consider a Person document in MongoDB:

{
"_id": "1", "email": "jane.doe@example.com", "phone": "1234567890" }, { "_id": "2", "email": "john.doe@example.com", "phone": null }

Requirements:

  1. The email field must always be unique and cannot be null.
  2. The phone field must allow multiple null values but should be unique for non-null values.

Approach 1: Using MongoDB Indexes

1. Compound Index with Partial Filters

You can use a compound index with partial filters to enforce these rules directly in MongoDB.

MongoDB Index Creation:

db.person.createIndex(
{ email: 1 }, { unique: true } ); db.person.createIndex( { phone: 1 }, { unique: true, partialFilterExpression: { phone: { $exists: true, $ne: null } } } );

Explanation:

  1. The first index ensures the email field is always unique.
  2. The second index applies uniqueness only to phone values that exist and are not null.

Pros:

  • Enforces constraints at the database level.
  • Native MongoDB functionality ensures better performance.

Cons:

  • Direct database constraints require coordination with application-level validation.

Approach 2: Spring Boot Validation Annotations

In Spring Boot WebFlux, you can use validation annotations to enforce constraints at the application level.

Entity Class Example:


import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.Indexed; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; public class Person { @Id private String id; @NotNull @Email @Indexed(unique = true) private String email; @Indexed(unique = true, sparse = true) private String phone; // Getters and Setters }

Annotations Used:

  • @Indexed(unique = true): Ensures uniqueness for the email and phone fields.
  • sparse = true: Allows multiple documents with null phone values.
  • @NotNull: Ensures email is always provided.

Validation Example:

If you want to validate these constraints reactively in the application, use a WebFlux controller with a validation framework.

Example Controller:


import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; @RestController @RequestMapping("/api/persons") @Validated public class PersonController { private final PersonRepository personRepository; public PersonController(PersonRepository personRepository) { this.personRepository = personRepository; } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Mono<Person> createPerson(@RequestBody @Valid Person person) { return personRepository.save(person); } }

Approach 3: Application-Level Validation

For more control, you can enforce constraints programmatically in your service layer.

Service Example:


import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @Service public class PersonService { private final PersonRepository personRepository; public PersonService(PersonRepository personRepository) { this.personRepository = personRepository; } public Mono<Person> createPerson(Person person) { return personRepository.findByEmailOrPhone(person.getEmail(), person.getPhone()) .flatMap(existing -> Mono.error(new RuntimeException("Duplicate email or phone found."))) .switchIfEmpty(personRepository.save(person)); } }

Repository Example:


import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import reactor.core.publisher.Mono; public interface PersonRepository extends ReactiveMongoRepository<Person, String> { Mono<Person> findByEmailOrPhone(String email, String phone); }

Comparing Approaches

MethodProsCons
MongoDB IndexesHigh performance, enforced at the database level.Limited flexibility; application logic needed for additional validation.
Spring Boot AnnotationsEasy to implement, aligns with application logic.Relies on proper integration with MongoDB's indexing to avoid inconsistency.
Application-Level ValidationFull control over constraints; allows for custom business logic.Additional complexity; prone to race conditions unless MongoDB transactions are used.

Best Practices

  1. Combine Indexes and Application Validation: Use MongoDB's native indexing for core constraints and supplement with application-level validation for complex rules.
  2. Handle Race Conditions: In concurrent environments, consider using transactions (@Transactional with MongoDB) to avoid duplicate entries.
  3. Test for Edge Cases: Ensure your solution handles null values, duplicate entries, and concurrent requests effectively.
  4. Document Schema: Clearly document your constraints and validation logic for better maintainability.

Topics for Further Exploration

  1. MongoDB Transactions: Learn how to use transactions in a reactive environment to ensure consistency.
  2. Reactive Programming with Spring Boot WebFlux: Understand how to handle non-blocking data validation and persistence.
  3. Schema Design Best Practices: Explore strategies for designing robust and scalable MongoDB schemas.
  4. Spring Data MongoDB: Dive into advanced features like custom queries and indexing.

By implementing these strategies, you can ensure that your application effectively enforces unique constraints while remaining scalable and maintainable.