PlayFramework
Morphia
Java
Web Development
NoSQL

PlayFramework with Morphia?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Play Framework can work with Morphia, but the integration is not as turnkey as using a data layer that already has first-class Play support. Morphia is a Java ODM for MongoDB, while Play focuses on HTTP handling, dependency injection, and application lifecycle. The combination is reasonable when you want a Java-centric MongoDB mapping layer and are willing to wire the components together explicitly.

Decide Whether Morphia Fits the Project

Before integrating anything, decide what problem Morphia is solving. It is useful when you want:

  • Java classes mapped directly to MongoDB documents
  • repository-style data access without writing raw BSON everywhere
  • indexes and collection metadata declared close to the entity classes

If your application mostly needs direct MongoDB driver access, Morphia can add unnecessary abstraction. If you want an ODM, however, it is a solid option.

The main design point is that Play will not manage Morphia automatically for you. You need to initialize the Mongo client, create the datastore, and inject a service or repository layer that controllers can use.

Create a Morphia Service Managed by Play

A common pattern is to build one singleton service that owns the MongoDB connection and exposes a Datastore.

java
1package services;
2
3import com.mongodb.client.MongoClient;
4import com.mongodb.client.MongoClients;
5import dev.morphia.Datastore;
6import dev.morphia.Morphia;
7import jakarta.inject.Inject;
8import jakarta.inject.Singleton;
9import play.Configuration;
10
11@Singleton
12public class MorphiaService {
13    private final MongoClient client;
14    private final Datastore datastore;
15
16    @Inject
17    public MorphiaService(Configuration config) {
18        String uri = config.getString("mongodb.uri");
19        String database = config.getString("mongodb.database");
20
21        this.client = MongoClients.create(uri);
22        this.datastore = Morphia.createDatastore(client, database);
23        this.datastore.getMapper().mapPackage("models");
24        this.datastore.ensureIndexes();
25    }
26
27    public Datastore datastore() {
28        return datastore;
29    }
30}

This gives Play a single injected component that can be reused by repositories.

Map Documents With Clear Domain Classes

Morphia works best when entity classes stay simple and focused.

java
1package models;
2
3import dev.morphia.annotations.Entity;
4import dev.morphia.annotations.Id;
5
6@Entity("users")
7public class User {
8    @Id
9    private String id;
10    private String email;
11    private String displayName;
12
13    public User() {}
14
15    public User(String id, String email, String displayName) {
16        this.id = id;
17        this.email = email;
18        this.displayName = displayName;
19    }
20
21    public String getId() { return id; }
22    public String getEmail() { return email; }
23    public String getDisplayName() { return displayName; }
24}

Keep validation and request-specific concerns out of the entity itself. Controllers should deal with HTTP input, repositories should deal with persistence, and the model should describe stored data.

Put Data Access Behind Repositories

Controllers should not build Morphia queries directly. A repository layer makes testing and future refactoring much easier.

java
1package repositories;
2
3import dev.morphia.Datastore;
4import jakarta.inject.Inject;
5import jakarta.inject.Singleton;
6import models.User;
7import services.MorphiaService;
8
9@Singleton
10public class UserRepository {
11    private final Datastore datastore;
12
13    @Inject
14    public UserRepository(MorphiaService morphiaService) {
15        this.datastore = morphiaService.datastore();
16    }
17
18    public void save(User user) {
19        datastore.save(user);
20    }
21
22    public User findByEmail(String email) {
23        return datastore.find(User.class)
24            .filter(dev.morphia.query.filters.Filters.eq("email", email))
25            .first();
26    }
27}

Now the controller can stay focused on request handling.

java
1package controllers;
2
3import jakarta.inject.Inject;
4import models.User;
5import play.mvc.Controller;
6import play.mvc.Result;
7import repositories.UserRepository;
8
9public class UserController extends Controller {
10    private final UserRepository repository;
11
12    @Inject
13    public UserController(UserRepository repository) {
14        this.repository = repository;
15    }
16
17    public Result create() {
18        User user = new User("u1", "[email protected]", "Alice");
19        repository.save(user);
20        return ok("saved");
21    }
22}

Handle Lifecycle and Configuration Explicitly

Play applications usually load configuration from application.conf. Put the MongoDB connection details there and keep environment-specific values outside code.

hocon
mongodb.uri = "mongodb://localhost:27017"
mongodb.database = "playdemo"

Also decide how the client should be closed on shutdown. In a real project, tie resource cleanup into Play's lifecycle hooks so the Mongo client is not leaked during reloads or restarts.

Common Pitfalls

  • Expecting a drop-in Play plugin experience when the integration actually needs explicit wiring.
  • Letting controllers depend directly on Morphia queries instead of using repositories or services.
  • Mixing HTTP validation, domain logic, and persistence mapping in the same class.
  • Hardcoding MongoDB connection details instead of using Play configuration.
  • Skipping lifecycle cleanup for the Mongo client in long-running environments.

Summary

  • Play and Morphia can work well together when you wire the integration deliberately.
  • Create one injected Morphia service that manages the client and datastore.
  • Keep entity classes simple and move query logic into repositories.
  • Let controllers focus on HTTP concerns, not database details.
  • Treat configuration and connection lifecycle as first-class integration work, not afterthoughts.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.