Introduction
This in-depth guide will walk you through building fully reactive CRUD REST APIs using Spring Boot, Spring WebFlux (for reactive programming), and Google Cloud Firestore (a NoSQL document database). We'll cover everything from setting up Google Cloud to testing with Postman.
Table of Contents
- Prerequisites and Setup
- Google Cloud Firestore Configuration
- Spring Boot Project Initialization
- Reactive Programming Fundamentals
- Implementing CRUD Operations
- Advanced Features
- Testing with Postman
- Deployment Considerations
1. Prerequisites and Setup
Required Tools:
- Java JDK 17+
- Maven 3.8+ or Gradle 7+
- Google Cloud account (free tier available)
- Postman or similar API testing tool
- IDE (IntelliJ, VS Code, Eclipse)
Google Cloud Project Setup:
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable billing (required for Firestore)
- Note your Project ID (visible on dashboard)
2. Google Cloud Firestore Configuration
2.1 Enable Firestore API
- Navigate to "APIs & Services" > "Library"
- Search for "Cloud Firestore API" and enable it
2.2 Create Firestore Database
- Go to "Firestore" in the left menu
- Click "Create Database"
- Choose "Native mode" (not Datastore mode)
- Select location (choose closest to your users)
- Set security rules (start in test mode for development)
2.3 Set Up Authentication
- Go to "IAM & Admin" > "Service Accounts"
- Create new service account or use default
- Add "Cloud Datastore User" role
- Generate JSON key file (save securely)
2.4 Initialize Firestore
Create a collection called "products" with sample documents:
{
"id": "prod001",
"name": "Premium Coffee",
"price": 12.99,
"stock": 100,
"categories": ["beverage", "coffee"]
}
3. Spring Boot Project Initialization
3.1 Create Project
Use Spring Initializr (https://start.spring.io/) with:
- Project: Maven/Gradle
- Language: Java
- Spring Boot: 3.2+
- Dependencies:
- Spring Reactive Web (WebFlux)
- Google Cloud Firestore
- Lombok
- Spring Data Reactive
Or use this curl command:
curl https://start.spring.io/starter.zip -d dependencies=webflux,cloud-gcp,lombok -d type=maven-project -d language=java -d bootVersion=3.2.0 -d baseDir=firestore-reactive-demo -o firestore-reactive-demo.zip
3.2 Project Structure
src/main/java/com/example/firestoredemo/ ├── config/ # Configuration classes ├── controller/ # Reactive REST controllers ├── model/ # Document model classes ├── repository/ # Reactive Firestore repositories ├── service/ # Reactive service layer └── FirestoreDemoApplication.java
3.3 Add Firestore Dependency
For Maven:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-data-firestore</artifactId>
<version>3.7.0</version>
</dependency>
For Gradle:
implementation 'com.google.cloud:spring-cloud-gcp-data-firestore:3.7.0'
4. Reactive Programming Fundamentals
4.1 Key Concepts
- Mono: 0-1 result (like Optional)
- Flux: 0-N results (like List)
- Publisher: Interface both Mono and Flux implement
- Subscriber: Receives and processes items
4.2 WebFlux vs Traditional MVC
- Non-blocking I/O
- Backpressure support
- Functional routing option
- Better scalability for high-load systems
5. Implementing CRUD Operations
5.1 Configuration
application.properties:
# Google Cloud Configuration
spring.cloud.gcp.firestore.project-id=${GCP_PROJECT_ID}
spring.cloud.gcp.firestore.credentials.location=classpath:service-account.json
# Application
server.port=8080
FirestoreConfig.java:
@Configuration
public class FirestoreConfig {
@Bean
public FirestoreTemplate firestoreTemplate(Firestore firestore) {
return new FirestoreTemplate(firestore, "your-database-id");
}
}
5.2 Document Model
Product.java:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collectionName = "products")
public class Product {
@Id
private String id;
private String name;
private Double price;
private Integer stock;
private List<String> categories;
private Instant createdAt;
@Document(createTime = true)
private Instant createTime;
}
5.3 Reactive Repository
ProductRepository.java:
public interface ProductRepository extends FirestoreReactiveRepository<Product> {
Flux<Product> findByPriceGreaterThan(Double price);
Flux<Product> findByCategoriesContaining(String category);
}
5.4 Service Layer
ProductService.java:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public Flux<Product> getAllProducts() {
return productRepository.findAll();
}
public Mono<Product> getProductById(String id) {
return productRepository.findById(id)
.switchIfEmpty(Mono.error(new ResourceNotFoundException("Product not found")));
}
public Mono<Product> createProduct(Product product) {
product.setId(UUID.randomUUID().toString());
product.setCreatedAt(Instant.now());
return productRepository.save(product);
}
public Mono<Product> updateProduct(String id, Product product) {
return productRepository.existsById(id)
.flatMap(exists -> exists ?
productRepository.save(product.withId(id)) :
Mono.error(new ResourceNotFoundException("Product not found")));
}
public Mono<Void> deleteProduct(String id) {
return productRepository.deleteById(id);
}
public Flux<Product> getProductsByCategory(String category) {
return productRepository.findByCategoriesContaining(category);
}
}
5.5 Reactive Controller
ProductController.java:
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Flux<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable String id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Product> createProduct(@RequestBody Product product) {
return productService.createProduct(product);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Product>> updateProduct(
@PathVariable String id, @RequestBody Product product) {
return productService.updateProduct(id, product)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteProduct(@PathVariable String id) {
return productService.deleteProduct(id);
}
@GetMapping("/category/{category}")
public Flux<Product> getProductsByCategory(@PathVariable String category) {
return productService.getProductsByCategory(category);
}
}
5.6 Exception Handling
GlobalErrorHandler.java:
@RestControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public Mono<ResponseEntity<String>> handleNotFound(ResourceNotFoundException ex) {
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public Mono<ResponseEntity<String>> handleGeneralException(Exception ex) {
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred: " + ex.getMessage()));
}
}
6. Advanced Features
6.1 Pagination
@GetMapping
public Flux<Product> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return productRepository.findAll()
.skip(page * size)
.take(size);
}
6.2 Caching with Caffeine
@Cacheable("products")
public Mono<Product> getProductById(String id) {
return productRepository.findById(id);
}
6.3 Real-time Updates with Firestore Listeners
public Flux<ProductChange> listenToProductChanges() {
return firestoreTemplate.listen(Product.class)
.map(change -> new ProductChange(change.getType(), change.getDocument()));
}
6.4 Transaction Support
public Mono<Void> updateStock(String productId, int quantity) {
return firestoreTemplate.runTransaction(transaction -> {
return transaction.get(firestoreTemplate.getFirestore()
.collection("products")
.document(productId))
.flatMap(documentSnapshot -> {
Product product = documentSnapshot.toObject(Product.class);
product.setStock(product.getStock() - quantity);
return transaction.set(documentSnapshot.getReference(), product);
});
});
}
7. Testing with Postman
7.1 Collection Setup
- Create new collection "Firestore Reactive API"
- Set base URL:
http://localhost:8080/api/products
7.2 Sample Requests
Create Product (POST):
{
"name": "Organic Tea",
"price": 8.99,
"stock": 50,
"categories": ["beverage", "tea"]
}
Get All Products (GET):
GET http://localhost:8080/api/products
Get Single Product (GET):
GET http://localhost:8080/api/products/{id}
Update Product (PUT):
{
"name": "Organic Tea Premium",
"price": 9.99,
"stock": 45,
"categories": ["beverage", "tea", "premium"]
}
Delete Product (DELETE):
DELETE http://localhost:8080/api/products/{id}
Filter by Category (GET):
GET http://localhost:8080/api/products/category/tea
7.3 Testing Reactive Streams
- Use Postman's "Server-Sent Events" for streaming endpoints
- Verify backpressure behavior with large datasets
- Test error scenarios (404, 500, etc.)
8. Deployment Considerations
8.1 Google Cloud Run
- Containerize your app with Docker
- Push to Google Container Registry
- Deploy to Cloud Run (fully managed)
8.2 Environment Variables
GCP_PROJECT_ID
: Your Google Cloud project IDGOOGLE_APPLICATION_CREDENTIALS
: Path to service account JSON
8.3 Monitoring
- Integrate with Google Cloud Operations (Stackdriver)
- Set up logging and metrics
- Configure alerts for errors
8.4 Scaling
- Cloud Run automatically scales based on load
- Configure minimum/maximum instances
- Consider Firestore scaling limits (1MB/document, 10,000 writes/second per database)
Final Notes
This implementation provides a fully reactive, scalable backend solution with:
- Non-blocking I/O throughout the stack
- Real-time database capabilities
- Clean architecture with separation of concerns
- Comprehensive error handling
- Production-ready features
For production use, additionally consider:
- API documentation with OpenAPI/Swagger
- Rate limiting
- Authentication/Authorization
- Input validation
- Comprehensive test suite