This chapter focuses on building the persistence layer for the Bookstore application using Spring Data JPA with PostgreSQL and Spring Data MongoDB with MongoDB. You'll learn how to configure databases, design JPA entities and MongoDB documents, create repository interfaces, and apply best practices for scalable microservices.
- Chapter Overview
- Understanding Database Types
- Building the Inventory Microservice (PostgreSQL + JPA)
- JPA Entities
- Building JPA Repositories
- JPA Quick Summary and Query-Derivation Notes
- Building the User Microservice (MongoDB + Spring Data MongoDB)
- MongoDB Documents
- MongoDB Repository
- Repository Best Practices (JPA vs MongoDB)
- Testing the Repository Layer
- Installation & Setup Steps
- Resources & References
This chapter provides comprehensive coverage of database persistence strategies for microservices. You'll learn:
Please confirm the required runtime dependencies before running this chapter:
- Confirm the database is started (PostgreSQL and MongoDB for this chapter).
- Confirm any infrastructure dependencies are running (for example Docker services, if used).
- Confirm any dependencies from previous chapters are running as needed for your flow.
docker ps | grep bookstore-postgresdocker ps | grep bookstore-mongodocker run -d \
--name bookstore-postgres \
-e POSTGRES_USER=bookstore \
-e POSTGRES_PASSWORD=bookstore123 \
-e POSTGRES_DB=inventory \
-p 5432:5432 \
postgres:17docker run -d \
--name bookstore-mongo \
-e MONGO_INITDB_ROOT_USERNAME=bookstore \
-e MONGO_INITDB_ROOT_PASSWORD=bookstore123 \
-e MONGO_INITDB_DATABASE=userDB \
-p 27017:27017 \
mongo:8The final source code for this chapter is already uploaded in this directory.
Use this folder as the reference implementation for the completed chapter state.
Microservices benefit from polyglot persistence, choosing the right database depending on service needs.
- Structured tables with strict schema
- Supports joins, normalization, and ACID transactions
- Ideal for Inventory (Books, Authors) with relational data
- Flexible schema with JSON-like documents
- Great for evolving and nested structures
- Ideal for User Profiles & Preferences with dynamic data
| Feature | PostgreSQL | MongoDB |
|---|---|---|
| Data Model | Tables | Documents |
| Schema | Strict | Flexible |
| Query Language | SQL | BSON |
| Transactions | Full ACID | ACID (with sessions) |
| Best For | Structured, relational data | Dynamic profile-like data |
| Scalability | Vertical + Horizontal | Horizontal |
docker run -d \
--name bookstore-postgres \
-e POSTGRES_USER=bookstore \
-e POSTGRES_PASSWORD=bookstore123 \
-e POSTGRES_DB=inventory \
-p 5432:5432 \
postgres:17Add to pom.xml:
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>spring:
datasource:
url: jdbc:postgresql://localhost:5432/inventory
username: bookstore
password: bookstore123
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
main:
allow-bean-definition-overriding: true
profiles:
active: dev
java:
version: 25
server:
port: 8081@Entity
@Table(name = "books")
@Data
public class Book extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(unique = true)
private String isbn;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private int quantity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
}@Entity
@Table(name = "authors")
@Data
public class Author extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>();
}@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class Auditable {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
// Derived query methods
List<Book> findByTitleContainingIgnoreCase(String keyword);
List<Book> findByAuthor_Name(String name);
List<Book> findByPriceBetween(BigDecimal min, BigDecimal max);
Optional<Book> findByIsbn(String isbn);
// Custom JPQL query
@Query("SELECT b FROM Book b WHERE b.quantity < :threshold")
List<Book> findLowStockBooks(@Param("threshold") int threshold);
// Native SQL query
@Query(value = "SELECT * FROM books WHERE price > :minPrice ORDER BY price DESC",
nativeQuery = true)
List<Book> findExpensiveBooks(@Param("minPrice") BigDecimal minPrice);
}@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
Optional<Author> findByName(String name);
List<Author> findByNameContainingIgnoreCase(String keyword);
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books WHERE a.id = :id")
Optional<Author> findByIdWithBooks(@Param("id") Long id);
}- For a focused summary of the main JPA components used in this chapter, see JPA.md.
- The derived query examples in this chapter are intentionally simple. In real projects, query derivation has naming rules, parser constraints, and edge cases.
- Derived methods are best for straightforward lookups; for complex filtering or clearer intent, prefer explicit
@Querymethods. - Validate derived queries with repository tests (
@DataJpaTest) so behavior is confirmed early.
| Scenario | Preferred Approach | Why |
|---|---|---|
Simple equality or small filters (findByEmail, findByTitleContaining) |
Derived query method | Fast to write, readable, and easy to maintain |
| Nested or lengthy method names becoming hard to read | @Query (JPQL) |
Makes intent explicit and avoids very long method signatures |
| Complex joins, grouped logic, or tuning for performance | @Query (JPQL or native SQL) |
Better control over query behavior and execution |
| Database-specific SQL feature is required | @Query(nativeQuery = true) |
Full SQL control for vendor-specific capabilities |
| Team is unsure about parser behavior or edge cases | @Query + repository tests |
Reduces ambiguity and verifies behavior explicitly |
- Official references for deeper understanding:
- Spring Data JPA: https://spring.io/projects/spring-data-jpa
- Spring Data JPA Reference: https://docs.spring.io/spring-data/jpa/reference/
- Query Methods Details: https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html
docker run -d \
--name bookstore-mongo \
-e MONGO_INITDB_ROOT_USERNAME=bookstore \
-e MONGO_INITDB_ROOT_PASSWORD=bookstore123 \
-e MONGO_INITDB_DATABASE=userDB \
-p 27017:27017 \
mongo:8Add to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>spring:
data:
mongodb:
host: localhost
port: 27017
database: userDB
username: bookstore
password: bookstore123
authentication-database: admin
main:
allow-bean-definition-overriding: true
profiles:
active: dev
java:
version: 25
server:
port: 8082@Document(collection = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private String id;
@Indexed(unique = true)
private String email;
@Indexed
private String username;
private Profile profile;
private Preferences preferences;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Profile {
private String fullName;
private String phoneNumber;
private Address address;
}@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
private String country;
}@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Preferences {
private List<String> favoriteGenres;
private boolean emailNotifications;
private boolean smsNotifications;
private String language;
}@Repository
public interface UserRepository extends MongoRepository<User, String> {
// Derived query methods
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
List<User> findByUsernameContainingIgnoreCase(String keyword);
// Nested field query
List<User> findByProfile_Address_City(String city);
// Custom JSON query
@Query("{ 'preferences.favoriteGenres': ?0 }")
List<User> findByFavoriteGenre(String genre);
// Query with projection
@Query(value = "{ 'email': ?0 }", fields = "{ 'profile': 1, 'email': 1 }")
Optional<User> findUserProfileByEmail(String email);
}| Practice | JPA | MongoDB |
|---|---|---|
| Single Result | Optional | Optional |
| Nested Fields | author.name | profile.address.city |
| Custom Queries | @Query JPQL | @Query JSON |
| Pagination | Pageable | Pageable |
| Indexing | DB indexes | @Indexed |
| Lazy Loading | @ManyToOne(LAZY) | N/A (embedded) |
| Transactions | @Transactional | @Transactional |
- Use
Optional<T>for single results to handle null safely - Prefer
fetch = FetchType.LAZYto avoid N+1 queries - Use
@Queryfor complex queries that can't be derived - Always test with
@DataJpaTestfor repository layer - Use pagination for large result sets
- Index frequently queried columns
- Use embedded documents for tightly coupled data
- Apply
@Indexedto frequently queried fields - Use projections to fetch only required fields
- Leverage MongoDB's flexible schema for evolving data
- Test with
@DataMongoTestfor repository layer - Consider compound indexes for complex queries
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Test
void testFindAll() {
// Given
Author author = new Author();
author.setName("John Doe");
authorRepository.save(author);
Book book = new Book();
book.setTitle("Test Book");
book.setIsbn("1234567890");
book.setPrice(new BigDecimal("29.99"));
book.setQuantity(10);
book.setAuthor(author);
bookRepository.save(book);
// When
List<Book> books = bookRepository.findAll();
// Then
assertThat(books).isNotEmpty();
assertThat(books).hasSize(1);
assertThat(books.get(0).getTitle()).isEqualTo("Test Book");
}
@Test
void testFindByTitleContaining() {
// Test implementation
}
@Test
void testFindLowStockBooks() {
// Test implementation
}
}@DataMongoTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void testFindByEmail() {
// Given
User user = User.builder()
.email("test@example.com")
.username("testuser")
.profile(Profile.builder()
.fullName("Test User")
.build())
.build();
userRepository.save(user);
// When
Optional<User> found = userRepository.findByEmail("test@example.com");
// Then
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo("testuser");
}
@Test
void testFindByFavoriteGenre() {
// Test implementation
}
@Test
void testFindByCity() {
// Test implementation
}
}Download from: https://www.docker.com/products/docker-desktop
docker run -d \
--name bookstore-postgres \
-e POSTGRES_USER=bookstore \
-e POSTGRES_PASSWORD=bookstore123 \
-e POSTGRES_DB=inventory \
-p 5432:5432 \
postgres:17docker run -d \
--name bookstore-mongo \
-e MONGO_INITDB_ROOT_USERNAME=bookstore \
-e MONGO_INITDB_ROOT_PASSWORD=bookstore123 \
-e MONGO_INITDB_DATABASE=userDB \
-p 27017:27017 \
mongo:8docker psAdd JPA dependencies to Inventory microservice and MongoDB dependencies to User microservice as shown in the sections above.
# Inventory microservice
cd inventory-service
./mvnw spring-boot:run
# User microservice
cd user-service
./mvnw spring-boot:run- Chapter JPA Summary: JPA.md
- Spring Data JPA Documentation: https://spring.io/projects/spring-data-jpa
- Spring Data MongoDB Documentation: https://spring.io/projects/spring-data-mongodb
- PostgreSQL Documentation: https://www.postgresql.org/docs/
- MongoDB Documentation: https://docs.mongodb.com/
- Docker Documentation: https://docs.docker.com/
- Lombok Documentation: https://projectlombok.org/