Anatomy of a Java Web App
A modern Java web application is organised into clear layers and a conventional directory layout so that each piece of code has an obvious home. At a high level, a typical Spring Boot project separates controllers (handle web requests), services (business logic), repositories (database access), and models/entities (the data), all wired together by the Spring container. Understanding this anatomy means you can open any Java project and immediately know where to look — and where your own code belongs.
The standard directory layout
Java projects built with Maven or Gradle follow a near-universal structure:
my-app/
├── pom.xml # build file: dependencies, plugins, build config
├── src/
│ ├── main/
│ │ ├── java/ # all application source code
│ │ │ └── in/infoplanet/app/
│ │ │ ├── Application.java # the main entry point
│ │ │ ├── controller/ # web layer (REST/MVC controllers)
│ │ │ ├── service/ # business logic
│ │ │ ├── repository/ # data access
│ │ │ └── model/ # entities / domain objects
│ │ └── resources/
│ │ ├── application.properties # configuration
│ │ ├── static/ # CSS, JS, images
│ │ └── templates/ # server-rendered views (if used)
│ └── test/
│ └── java/ # tests mirror the main package layout
The split between main and test, and between java and resources, is a convention every Java tool understands. Configuration and assets live in resources; code lives in java.
The layered architecture
Inside the code, responsibilities are layered. A request flows downward and a response flows back up:
- Controller layer — receives the HTTP request, validates input, and delegates. It does not contain business logic.
- Service layer — the brain: business rules, calculations, coordinating multiple repositories, managing transactions.
- Repository layer — talks to the database (often via Spring Data JPA), returning entities.
- Model layer — the entities and value objects that represent your data.
Each layer depends only on the one below it. Controllers call services; services call repositories. Skipping a layer (a controller hitting the database directly) is what creates tangled, untestable code.
How a request flows
Here is a thin controller showing how the layers connect — notice it does almost nothing except delegate:
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService service; // depends on the service layer only
public StudentController(StudentService service) {
this.service = service;
}
// 1. Controller receives the request
@GetMapping("/{id}")
public Student getStudent(@PathVariable long id) {
// 2. Delegates to the service (which calls the repository, which hits the DB)
return service.findById(id);
// 3. The returned object is serialized to JSON and sent back
}
}
The controller never touches SQL or the database — it asks the service, which orchestrates everything below. This is the layered architecture in action.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesPackage by layer vs package by feature
Two common ways to organise packages:
- By layer —
controller,service,repositorypackages (shown above). Simple and familiar for smaller apps. - By feature — packages like
students,courses,fees, each containing its own controller, service, and repository. This scales better in large apps because related code stays together.
Either is valid; consistency within a project matters more than the choice.
Where configuration lives
Application settings (port, database URL, feature flags) live in application.properties or application.yml under resources. Build configuration (dependencies, plugins) lives in pom.xml (Maven) or build.gradle (Gradle). Keeping these separate from code means you can change behaviour per environment without recompiling.
Common mistakes
- Letting controllers contain business logic. Keep them thin; the service layer is where logic belongs, so it stays testable and reusable.
- Calling repositories directly from controllers. This skips the service layer and couples web concerns to data access.
- Scattering classes outside the scanned package. Spring scans from the main class's package downward; misplaced classes will not be picked up.
- Putting config values in code. Hard-coding the database URL or port makes the app un-deployable across environments. Use
application.properties. - Inconsistent package strategy. Mixing package-by-layer and package-by-feature randomly makes the project hard to navigate.
FAQ
Is this layout the same for non-Spring apps? The Maven/Gradle directory structure is universal across Java. The layered packages are a best practice that most frameworks encourage.
Where do tests go?
Under src/test/java, mirroring the main package structure so each class's tests are easy to find.
Keep learning
This structure organises the MVC pattern and is generated for you by Spring Boot. When you are ready, learn how to deploy a Java web app. More in the Advanced Java hub.
Want to build well-structured Java projects from scratch? Join the waitlist for the Advanced Java course at Infoplanet, Jalgaon.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesFounder, Infoplanet
Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.
Related guides
Advanced Java / Backend Roadmap
A clear, ordered roadmap for learning Advanced Java and backend development — from core Java through servlets, Spring Boot, REST APIs, and deployment.
Deploying a Java Web App
A practical guide to deploying a Spring Boot app: build an executable JAR, run it on a server, containerise with Docker, and check production essentials.
Hibernate & JPA Basics
Hibernate is the leading JPA implementation that maps Java objects to database tables. This guide explains ORM, entities, and the Jakarta Persistence annotations.
