Relationships & Transactions
Grow TaskFlow into users, projects, and tasks; map object references to foreign keys; add owner-scoped queries; and use @Transactional for real units of work.
Assumes you know
After this you should be able to
- Map entity relationships
- Define a transaction boundary
Spring Boot by Building · Chapter 10
TaskFlow becomes a real multi-entity app: users own projects, projects contain tasks, and service methods protect the rule that work belongs to the right owner. This is where JPA stops being CRUD and starts becoming application design.
Choose your route#
Study the same Relationships & Transactions explanation with a different purpose: build and verify it in Learn deeply, or retrieve and defend it in Prepare for interviews.
- Map entity relationships
- Define a transaction boundary
Shared core#
The chapter below remains the canonical explanation for both goals. Keep its model, examples, and trade-offs shared; only the deliberate practice changes with the route.
Learn deeply: build evidence#
Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.
Prepare for interviews: explain the trade-off#
Without notes, explain the boundary this chapter protects, one concrete implementation decision, and the cost of a tempting shortcut. Then answer one follow-up using the chapter’s own example.
A relationship is an object reference that becomes a foreign key. A transaction is a promise that a service method's database changes commit together or roll back together. Put together, they let you express business rules in Java while the database keeps the data honest.
Section 1The new TaskFlow model#
@Entity
class AppUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
}
@Entity
class Project {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(optional = false)
private AppUser owner;
}
@Entity
class Task {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private boolean done;
@ManyToOne(optional = false)
private Project project;
}Projecthas anAppUser owner. JPA turns that reference intoowner_id.Taskhas aProject project. JPA turns that reference intoproject_id.- You do not need bidirectional lists yet. Start with the direction your code actually queries.
Section 2Mission: add projects and task ownership#
Goal. A task is created inside a project, and listing tasks for a project returns only that project's tasks.
POST /projectscreates a project with a name.POST /projects/{projectId}/taskscreates a task under that project.GET /projects/{projectId}/taskslists only tasks for that project.- Creating a task for a missing project returns 404.
- No controller directly calls a repository.
Likely files: model/Project.java, ProjectRepository.java,
TaskRepository.java, ProjectService.java, TaskService.java,
ProjectController.java.
Commit checkpoint: feat: add project task relationships
Hint 1 — repository method name
Task has a field named project, Spring Data can derive
findByProjectId(Long projectId).Hint 2 — service flow
Reference patch — service and repository shape
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByProjectId(Long projectId);
}
@Service
public class TaskService {
private final TaskRepository tasks;
private final ProjectRepository projects;
public TaskService(TaskRepository tasks, ProjectRepository projects) {
this.tasks = tasks;
this.projects = projects;
}
public Task createInProject(Long projectId, String title, boolean done) {
Project project = projects.findById(projectId).orElseThrow(() -> new ProjectNotFoundException(projectId));
Task task = new Task();
task.setTitle(title);
task.setDone(done);
task.setProject(project);
return tasks.save(task);
}
public List<Task> listForProject(Long projectId) {
if (!projects.existsById(projectId)) {
throw new ProjectNotFoundException(projectId);
}
return tasks.findByProjectId(projectId);
}
}Section 3Transactions: one service method, one unit of work#
Use @Transactional when one service method changes multiple rows and those changes must succeed or
fail together. Do not scatter it on every class by habit; put it where the business operation needs one unit.
@Transactional
public Project createProjectWithFirstTask(Long ownerId, String projectName, String firstTaskTitle) {
AppUser owner = users.findById(ownerId).orElseThrow(() -> new UserNotFoundException(ownerId));
Project project = new Project();
project.setName(projectName);
project.setOwner(owner);
Project saved = projects.save(project);
Task task = new Task();
task.setTitle(firstTaskTitle);
task.setProject(saved);
tasks.save(task);
return saved;
}If saving the task fails, the project insert should roll back too. That is the transaction boundary.
Section 4Mission: prepare TaskFlow for security#
Goal. Projects belong to users now, even before login exists. Chapter 11 will
replace the temporary ownerId parameter with the authenticated user.
POST /userscreates anAppUserwith unique email.POST /users/{userId}/projectscreates a project owned by that user.GET /users/{userId}/projectsreturns only that user's projects.- Project listing uses a repository query such as
findByOwnerId.
Commit checkpoint: feat: scope projects by owner
Hint — future security connection
userId only temporarily. After Basic Auth/JWT, the current user comes
from SecurityContext, but the service rule stays the same: query by owner.Reference patch — owner query
public interface ProjectRepository extends JpaRepository<Project, Long> {
List<Project> findByOwnerId(Long ownerId);
}
public List<Project> listForOwner(Long ownerId) {
if (!users.existsById(ownerId)) {
throw new UserNotFoundException(ownerId);
}
return projects.findByOwnerId(ownerId);
}Section 5N+1 awareness without panic#
JPA can accidentally run one query for the parent rows and then one query per child row. That is the N+1 problem. You do not need to master performance tuning today; you only need to recognize the smell.
Keep response DTOs small. Do not automatically serialize whole entity graphs. When a screen needs a nested view, write a query or service method for that exact view instead of returning every relationship blindly.
Notebook Summary#
@ManyToOneis the first relationship to reach for: many tasks belong to one project.- Foreign keys are object references in Java and columns in SQL.
@Transactionalbelongs on service operations that must commit or roll back as one unit.- Owner-scoped queries are the bridge to security. Authentication supplies the owner; services enforce the rule.
Recall prompts#
- Why start with unidirectional relationships?
- What should happen if project creation succeeds but first-task creation fails?
- Why is returning entities with relationships risky for JSON APIs?
Chapter 11 — Security and Basic Auth. Now that projects have owners, the API can stop trusting
userId in the URL and start using the authenticated user.
TaskFlow checkpoint#
Current state: tasks are loose records behind a safe boundary. Outcome: project ownership and an all-or-nothing service operation. Prerequisite: JPA associations. Java bridge: object references model relationships before JPA maps them. Estimate: 40 minutes.
Mission: create a project with its first task safely#
Add project ownership, scope reads through a repository query, and make the create-project-plus-first-task operation transactional.
- A project response contains only its own tasks.
- A failure creating the first task leaves no partial project.
- Curl can show owner-scoped project data.
Likely files: model/Project.java, repository/ProjectRepository.java, service/ProjectService.java. Suggested commit: feat: add taskflow project ownership.
Hint 1
Hint 2
Hint 3
Before opening the solution: force the second write to fail and inspect what remains.
Reference patch — project transaction
Recall before continuing#
- Which layer owns a transaction boundary?
- What should a rollback protect?
- Why scope projects by owner?
The next workshop proves the seams with fast service and controller tests before later changes add more cross-cutting behaviour.