Who's never worked on a spaghetti code? Luck you.
I've worked on sever projects that had some spaghetti code. And all this started because of a lack of structure.
A well-structured application helps you avoid that mess by clearly separating concerns. One common and effective approach is using a layered architecture.
While the number of layers can vary depending on your domain, the core idea is the same: group logic by responsibility. Here's how I typically split it.
Presentation Layer: Preparing Data for the Outside World
This is the outermost layer — the point of contact between your app and the external world (HTTP clients, frontend templates, etc.). Its job is not to contain business logic, but rather to orchestrate calls and shape responses.
In a REST API:
- Format data for output
- Call services to execute operations
- Parse query parameters or request payloads
And by definition (as I've never really working with this configuration), in a server-side rendered app:
- Generate HTML views
- Pass the right data to the templating engine
- Handle form submissions and redirects
Example:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
Order order = orderService.getOrderById(id);
return ResponseEntity.ok(OrderDTO.from(order));
}
}
Business Layer: Where the Real Logic Lives
This is the brain of your application — the place for business rules, calculations, and multi-entity coordination. If a rule involves more than one repository or touches more than one model, it belongs here.
Think of this layer as:
- Enforcing workflows (e.g., "A user can’t cancel an order after it's shipped")
- Coordinating between domain entities
- Transforming data across models
Example:
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
public OrderService(OrderRepository orderRepository, InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
}
public Order placeOrder(CreateOrderRequest request) {
inventoryService.reserveItems(request.getItems());
Order order = new Order(request.getCustomerId(), request.getItems());
return orderRepository.save(order);
}
}
If your business logic only affects a single entity, it may belong in the data layer instead.
Data Layer: Talking to the Database
This layer handles persistence, retrieval, and validation tied to single entities. It’s where you isolate your database logic and avoid scattering queries throughout your app.
Responsibilities:
- CRUD operations
- Database validation (e.g., uniqueness)
- Mapping between entities and tables
Example or a Spring repository:
public interface OrderRepository extends JpaRepository<Order, Long> {
Optional<Order> findByCustomerId(Long customerId);
}
If you're manually managing SQL, this layer is where your JDBI or JPA queries go. The key idea: don't leak persistence details into your service layer.
And here is an example of a entity:
public class Order {
public Long id;
public Long customerId;
public List<Item> items;
/* Getters and setters */
public float computeTotalPrice() {
if (this.items == null) {
return 0.0;
}
return (float) this.items.mapToDouble(Item::getPrice).sum();
}
}
You don't need to create a service like SingleOrderService to manage the logic that can go inside the object. Having object just as a POJO (Plain Old Java Objects) is so 2010.
Define Your Structure Early
One of the most effective things you can do on a new project is define your package structure and layer responsibilities from the start. Communicate this with your team. Make it explicit.
Here’s one possible layout:
src/
├── presentation/
│ └── controllers/
├── business/
│ └── services/
├── data/
│ └── repositories/
│ └── entities/
Call it out in your README or internal documentation: “Controllers go here, services go there, don’t skip the layering.”
Trust the Layered Architecture
Layered architecture isn't a silver bullet — but it works. It's predictable, scalable, and helps new devs onboard faster. Whether you're building a REST API or a full-stack web app, keeping your concerns cleanly separated is one of the best ways to stay productive long-term.
And if you're ever unsure where a piece of code belongs, ask yourself:
"Does this logic involve more than one entity?"
If yes — it probably belongs in the business layer.