In a microservices architecture, the database-per-service pattern means each microservice owns its own private database instead of sharing a single central DB. Other services can only access that data through the service’s API, never by querying its tables or collections directly.

This pattern gives you:

  • Better service independence and encapsulation.
  • Freedom to choose the best database type (SQL, NoSQL, etc.) per service.
  • Easier independent scaling and fault isolation when one database has issues.

In this post, you will build a small “Plant Care” microservice using:

  • Java Spring Boot
  • Spring Web + Spring Data MongoDB
  • MongoDB as the service’s dedicated database

The domain: a Plant Service that lets users create plants and track when each plant needs to be watered.


Architecture: Plant Care microservice in a microservices world

Imagine a small smart-home system with several microservices:

  • Plant Service (our focus) – manages plants, species, and watering schedules; uses MongoDB.
  • User Service – manages accounts and profiles; could use PostgreSQL.
  • Notification Service – sends email or push reminders; maybe uses another data store.

Using the database-per-service pattern:

  • Each service has its own database (Plant DB, User DB, Notification DB).
  • Each database is private to the owning service and is not shared.

For this tutorial, you will focus on the Plant Service only:

  • Expose REST endpoints like /api/plants for CRUD operations.
  • Store plant documents (name, species, wateringFrequencyDays, lastWateredDate) in MongoDB.

Flowchart: How a request moves through the system

Below is a conceptual flow for a request to the Plant Service in a microservices architecture that uses database-per-service.

Flow overview:

  • The client (web or mobile app) sends a request like GET /api/plants.
  • The API Gateway routes the request to the Plant Service.
  • The Plant Service (Spring Boot) calls its own MongoDB database to read or write plant data.
  • Parallel services like User Service and Notification Service exist, each with its own database, but they are not on this specific request path.

You can recreate this flowchart for your blog with:

  • Boxes: Client → API Gateway → Plant Service → Plant MongoDB
  • Side boxes: User Service → User DB, Notification Service → Notification DB

Spring Boot MongoDB microservice database per service

Project setup: Spring Boot + MongoDB

To keep this beginner friendly, this project uses only the essentials.​

Dependencies (Maven)

Use Spring Initializr or add the following dependencies:

  • spring-boot-starter-web – build REST APIs.​
  • spring-boot-starter-data-mongodb – integrate with MongoDB.

pom.xml (core parts only):

<dependencies>
    <!-- REST API support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MongoDB integration -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>

    <!-- Optional: validation support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- Test dependencies (optional for this post) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
XML

Spring Data MongoDB lets you define repository interfaces, and it generates implementations automatically for basic CRUD operations.

Configuration (application.properties)

For local development with a MongoDB instance running on default port:

spring.application.name=plant-service

server.port=8081

spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=plant_service_db
Bash

This configuration ensures the Plant Service uses its own MongoDB database, plant_service_db, separate from any other service’s database.


Domain model: Plant document

Each plant will be stored as a MongoDB document in a plants collection.

Fields:

  • id – unique identifier (MongoDB ObjectId)
  • name – name given by the user
  • species – optional species information
  • wateringFrequencyDays – how often the plant should be watered
  • lastWateredDate – when the plant was last watered

Plant.java:

package com.example.plantservice.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.LocalDate;

@Document(collection = "plants")
public class Plant {

    @Id
    private String id;

    private String name;
    private String species;
    private int wateringFrequencyDays;
    private LocalDate lastWateredDate;

    public Plant() {
    }

    public Plant(String name, String species, int wateringFrequencyDays, LocalDate lastWateredDate) {
        this.name = name;
        this.species = species;
        this.wateringFrequencyDays = wateringFrequencyDays;
        this.lastWateredDate = lastWateredDate;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSpecies() {
        return species;
    }

    public void setSpecies(String species) {
        this.species = species;
    }

    public int getWateringFrequencyDays() {
        return wateringFrequencyDays;
    }

    public void setWateringFrequencyDays(int wateringFrequencyDays) {
        this.wateringFrequencyDays = wateringFrequencyDays;
    }

    public LocalDate getLastWateredDate() {
        return lastWateredDate;
    }

    public void setLastWateredDate(LocalDate lastWateredDate) {
        this.lastWateredDate = lastWateredDate;
    }
}
Java

Spring Data maps this Java class to a BSON document in MongoDB and handles conversions for simple types.

p.s. you could use Lombok and reduce all that boilerplate code.


Repository: Talking to MongoDB the easy way

Spring Data MongoDB lets you create a repository interface that looks very simple but supports powerful operations.

PlantRepository.java:

package com.example.plantservice.repository;

import com.example.plantservice.model.Plant;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface PlantRepository extends MongoRepository<Plant, String> {

    List<Plant> findByNameContainingIgnoreCase(String name);

    List<Plant> findBySpeciesIgnoreCase(String species);
}
Java

Key points for beginners:

  • MongoRepository<Plant, String> gives CRUD methods like findAllfindByIdsavedeleteById.
  • Custom query methods like findByNameContainingIgnoreCase are derived from the method name.

Service layer: Business logic for plant care

The service layer keeps controllers clean and centralizes business rules, such as updating lastWateredDate.

PlantService.java:

package com.example.plantservice.service;

import com.example.plantservice.model.Plant;
import com.example.plantservice.repository.PlantRepository;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;

@Service
public class PlantService {

    private final PlantRepository plantRepository;

    public PlantService(PlantRepository plantRepository) {
        this.plantRepository = plantRepository;
    }

    public List<Plant> getAllPlants() {
        return plantRepository.findAll();
    }

    public Plant getPlantById(String id) {
        return plantRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Plant not found with id: " + id));
    }

    public Plant createPlant(Plant plant) {
        if (plant.getLastWateredDate() == null) {
            plant.setLastWateredDate(LocalDate.now());
        }
        return plantRepository.save(plant);
    }

    public Plant updatePlant(String id, Plant updated) {
        Plant existing = getPlantById(id);
        existing.setName(updated.getName());
        existing.setSpecies(updated.getSpecies());
        existing.setWateringFrequencyDays(updated.getWateringFrequencyDays());
        existing.setLastWateredDate(updated.getLastWateredDate());
        return plantRepository.save(existing);
    }

    public void deletePlant(String id) {
        plantRepository.deleteById(id);
    }

    public List<Plant> searchByName(String name) {
        return plantRepository.findByNameContainingIgnoreCase(name);
    }

    public Plant markAsWatered(String id) {
        Plant plant = getPlantById(id);
        plant.setLastWateredDate(LocalDate.now());
        return plantRepository.save(plant);
    }
}
Java

This service is where you could later add logic to compute “next watering date” and filter plants that are due for watering.


Controller: Exposing the REST API

The controller receives HTTP requests and delegates work to the service.​

PlantController.java:

package com.example.plantservice.controller;

import com.example.plantservice.model.Plant;
import com.example.plantservice.service.PlantService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/plants")
public class PlantController {

    private final PlantService plantService;

    public PlantController(PlantService plantService) {
        this.plantService = plantService;
    }

    @GetMapping
    public ResponseEntity<List<Plant>> getAllPlants() {
        return ResponseEntity.ok(plantService.getAllPlants());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Plant> getPlantById(@PathVariable String id) {
        return ResponseEntity.ok(plantService.getPlantById(id));
    }

    @PostMapping
    public ResponseEntity<Plant> createPlant(@RequestBody Plant plant) {
        Plant created = plantService.createPlant(plant);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Plant> updatePlant(@PathVariable String id,
                                             @RequestBody Plant plant) {
        return ResponseEntity.ok(plantService.updatePlant(id, plant));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deletePlant(@PathVariable String id) {
        plantService.deletePlant(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/search")
    public ResponseEntity<List<Plant>> searchByName(@RequestParam String name) {
        return ResponseEntity.ok(plantService.searchByName(name));
    }

    @PostMapping("/{id}/water")
    public ResponseEntity<Plant> markAsWatered(@PathVariable String id) {
        return ResponseEntity.ok(plantService.markAsWatered(id));
    }
}
Java

Example JSON payload for creating a plant:

{
  "name": "Living Room Fern",
  "species": "Fern",
  "wateringFrequencyDays": 3,
  "lastWateredDate": "2025-12-20"
}
JSON

Main application class

PlantServiceApplication.java:

package com.example.plantservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PlantServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(PlantServiceApplication.class, args);
    }
}
Java

This class boots the entire Spring context, including web and MongoDB configuration.


Where database-per-service really shows up

With this Plant Service complete, imagine adding other services:

ServiceResponsibilityDatabase technologyData access rule
Plant ServicePlants, species, watering scheduleMongoDBOnly via Plant Service API. 
User ServiceUsers, credentials, profilesRelational (e.g. Aurora/PostgreSQL) Only via User Service API. 
Notification ServiceEmail/push notifications and logsNoSQL / key-valueOnly via Notification API. 

The database-per-service pattern means:

  • No service is allowed to reach into another service’s database directly.
  • Each service can evolve its schema, scale independently, and choose the best storage technology.

In a real system, you might orchestrate cross-service operations with events or sagas rather than distributed transactions.

How to run and test locally

To run this beginner-friendly microservice locally:

  1. Run the Spring Boot
    mvn spring-boot:run
  2. Test endpoints (for example with curl or Postman):
    • Start MongoDB
      docker run -d --name mongo \ -p 27017:27017 \ mongo:latest
    • curl -X POST http://localhost:8081/api/plants \ -H "Content-Type: application/json" \ -d '{"name":"Bedroom Cactus","species":"Cactus","wateringFrequencyDays":14}'
    • curl http://localhost:8081/api/plants
    • curl -X POST http://localhost:8081/api/plants/{id}/water

Spring Boot and Spring Data MongoDB handle most of the heavy lifting for REST and persistence, which makes this pattern approachable even for beginners.


If you want, the next step can expand this into a small fleet: add a User Service and Notification Service, then show how they communicate with events while still respecting the database-per-service pattern.

Real world example

To ground this in real-world practice, it also helps to briefly touch on prior experience with similar architectures. During time on the platform team at one of Australia’s biggest banks, most microservices were built with Spring Boot and MongoDB as the core stack for domain APIs and internal platforms. That work involved standardizing patterns like database-per-service, enforcing clear service boundaries, and providing shared tooling and guardrails so product teams could move faster without sacrificing security or reliability. Bringing those lessons into this Plant Service example makes the design choices feel less academic and more like the pragmatic patterns used in large, highly regulated environments.

Conclusion

A well-designed microservice is more than just a collection of controllers and repositories; it is a clean boundary around a business capability with its own data, rules, and contracts. In this post, you walked through building a simple Plant Service using Spring Boot and MongoDB, and saw how the database-per-service pattern keeps that boundary clear and enforced. By giving the Plant Service its own database and exposing that data only through its REST API, you gain flexibility in technology choices, better isolation, and the ability to evolve and scale the service independently over time.

From here, you can extend the example in several directions: add authentication, introduce a User or Notification Service, or start emitting domain events to coordinate work across services without coupling their databases. As you do, keep the same guiding principle in mind: each microservice owns its data, its logic, and its API. If you stick to that discipline, patterns like database-per-service stop being theoretical and start becoming a practical foundation for building resilient, maintainable systems in the real world.

While you are here, maybe try one of my apps for the iPhone.

Snap! I was there on the App Store

If you enjoyed this guide, don’t stop here — check out more posts on AI and APIs on my blog (From https://mydaytodo.com/blog);

Build a Local LLM API with Ollama, Llama 3 & Node.js / TypeScript

Beginners guide to building neural networks using synaptic.js

Build Neural Network in JavaScript: Step-by-Step App Tutorial – My Day To-Do

Build Neural Network in JavaScript with Brain.js: Complete Tutorial


0 Comments

Leave a Reply

Verified by MonsterInsights