Layered Architecture in Java: A Practical Guide to Keeping Your Code Clean

posted Originally published at sergiolema.dev 3 min read

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.

If you read this far, tweet to the author to show them you care. Tweet a Thanks

I always say, writing hard(code) is easy, easy is hard. Better structure = Better product

It's harder to read code than to write code. Having a well structured project is easy (easier) to read.

More Posts

Clean, maintainable code practices in Node.js and Java with real-world examples and principles.

Nigel DSouza - Jun 27

Mastering Java Memory Management: A Comprehensive Guide to JVM Internals, Garbage Collection, and Optimization

Aditya Pratap Bhuyan - May 6

How to Filter a Collection Using Streams in Java?

Aditya Pratap Bhuyan - Jun 8

Facade Pattern provides a simplified interface to complex subsystems, hiding implementation details behind a clean API.

Hussein Mahdi - Apr 9

Huffman Encoding Algorithm Using a Greedy Approach in Java

Aditya Pratap Bhuyan - Jun 15
chevron_left