Skip to main content

Backend design - MORE CRUD!

Designing the Backend Domain - JPA, Relations, and Queries

This phase of the project has focused entirely on building a solid backend foundation using Java, JPA, Hibernate, and PostgreSQL.

The goal was not to build features quickly, but to design a domain model and persistence layer that:

  • reflects real-world relationships
  • is easy to reason about
  • can be extended safely as new technologies are introduced later in the semester

Domain-driven database design with JPA

Instead of starting with SQL tables, I designed the system around domain concepts, letting JPA handle table creation and relationships.

The core entities in the system are:

  • Company
  • User
  • Membership
  • MembershipType
  • Location
  • CheckIn

Each entity maps directly to a database table using JPA annotations.

This approach helped me think in objects and relationships, rather than rows and columns.

Primary keys and identity

All entities use surrogate primary keys:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Using generated IDs simplifies relationships and avoids coupling the domain model to business-specific identifiers.

Foreign keys through object references

Instead of storing foreign key IDs manually, relationships are represented using object references.

For example, each user belongs to a company:

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "company_id", nullable = false)
private Company company;

This creates a foreign key in the database (company_id) while allowing the Java code to work with objects rather than IDs.

Hibernate handles the translation automatically.

This was a key learning point: well-designed object relationships drastically reduce boilerplate code.

Separating membership data from users

Rather than placing membership-related fields directly on the User entity, membership data is modeled as its own entity.

A Membership represents a time-bound relationship between:

  • a User
  • a MembershipType

It also includes:

  • startDate
  • endDate
  • a MembershipStatus enum

This design allows:

  • membership history
  • cancellations and pauses
  • reactivation without data loss

Separating this logic early prevents destructive updates and keeps the model flexible.

Enums and database safety

Enums such as Role and MembershipStatus are stored using:

@Enumerated(EnumType.STRING)

Storing enums as strings:

  • improves database readability
  • prevents bugs caused by enum reordering
  • makes debugging and querying easier

DAO structure and responsibility

Each entity has a corresponding DAO implementing a shared IDAO<T> interface, providing basic CRUD functionality.

More complex queries are implemented as specific methods in the DAO where they logically belong.

For example:

  • UserDAO handles user-related queries
  • MembershipDAO handles membership-related business logic

Even when a membership-related query returns User objects, it still belongs in MembershipDAO, because the definition of “active” depends on membership state.

This separation keeps responsibilities clear and avoids bloated DAO classes.

JPQL and joins

JPQL was used instead of raw SQL to query the database.

JPQL operates on:

  • entities
  • fields
  • relationships

Rather than tables and columns. Example: finding active members of a company

SELECT DISTINCT u
FROM Membership m
JOIN m.user u
WHERE u.company.id = :companyId
  AND m.status = :status

This query:

  • joins Membership and User
  • filters by company
  • filters by membership status
  • returns users directly

The use of DISTINCT ensures that users are not duplicated if multiple records exist.

Managing entity state with JPA

When creating or updating entities, related objects may be detached from the persistence context.

To handle this safely, related entities are merged explicitly:

Company managedCompany = em.merge(user.getCompany());
user.setCompany(managedCompany);

This ensures Hibernate works with managed entities and avoids persistence errors.

Understanding managed vs detached entities was one of the more important lessons in this phase.

Manual integration testing via Main

Before introducing automated tests, a Main class is used to manually test the persistence layer.

This setup:

  • creates realistic test data
  • verifies entity mappings
  • tests updates and deletes
  • validates JPQL queries

While not a replacement for proper testing, this approach provided valuable insight into how Hibernate behaves at runtime.

Key takeaways

Some of the most important lessons from this phase:

  • JPA relationships are easier to reason about than manual foreign keys
  • A clean domain model simplifies queries and future features
  • JPQL encourages domain-oriented thinking
  • Correct planning of relations pays off immediately

The current state of the backend provides a stable foundation for introducing service layers, validation, REST endpoints, and security in the next stages of the project.