Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
296: Layered Architecture in Java Applications
A few years ago, I worked with a developer—let's call him Sam—who was building a high-frequency trading dashboard. Sam was moving fast, and in his haste, he put everything in the Controller. The code that handled the HTTP request also calculated the portfolio risk, formatted the currency, and executed the SQL queries to fetch historical data. For a week, it worked beautifully. Then, the business decided they needed the same risk calculations for a scheduled nightly email report. Sam realized that to reuse that logic, he had to either instantiate an HTTP request object manually (which is a nightmare) or copy-paste 200 lines of code into a new cron job. He spent the next three days untangling a "spaghetti" mess that should have taken an hour to organize.
This is why we use layered architecture. When you're starting out, creating three different classes to do one thing feels like "boilerplate" or over-engineering. I get it. But the goal isn't to write more code; it's to ensure that the part of your app that talks to the internet doesn't know—and doesn't care—how your database is structured.
The Danger of the Fat Controller
In a Java application, the "Presentation Layer" (your Controllers or API endpoints) should be thin. Think of the Controller as a receptionist. A receptionist doesn't perform surgery or manage the company's payroll; they take a message, make sure the caller is who they say they are, and hand the request to the right professional.
When you put business logic in the Controller, you've created a "Fat Controller." If you ever decide to switch from a REST API to a GraphQL interface, or if you need to trigger a process via a Message Queue (like RabbitMQ), you'll find your logic trapped. By stripping the Controller down to only handling request mapping and basic input validation, you make your application flexible.
Bridging the Gap with Service Layers
The "Service Layer" is where the actual "brain" of your application lives. This is where you implement your business rules. For example, if you're building an Order system, the Service layer is where you check if a customer has enough credit before allowing a purchase. It doesn't care if the request came from a mobile app or a web browser; it just knows how to "Process an Order."
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryClient inventoryClient;
public OrderService(OrderRepository orderRepository, InventoryClient inventoryClient) {
this.orderRepository = orderRepository;
this.inventoryClient = inventoryClient;
}
public Order placeOrder(OrderRequest request) {
// Business Logic: Check inventory before saving
if (!inventoryClient.isAvailable(request.getProductId())) {
throw new OutOfStockException("Item is unavailable");
}
Order order = new Order(request.getCustomerId(), request.getProductId());
return orderRepository.save(order);
}
}
Notice how the OrderService doesn't mention HttpServletRequest or ResponseEntity. It deals in pure Java objects. This means I can write a JUnit test for this logic in milliseconds without needing to start a whole web server.
Isolating the Data Access Layer
Finally, we have the Data Access Layer (or Repository layer). Its only job is to talk to the database. It should be a thin wrapper around your SQL queries or your JPA repositories. I've seen developers try to put "business logic" here—like calculating a discount inside a SQL query. Avoid that. Keep your repositories "dumb." They should do four things: Create, Read, Update, and Delete.
By separating these three—Controller (Reception), Service (Brain), and Repository (Librarian)—you create a unidirectional flow of dependencies. The Controller depends on the Service, and the Service depends on the Repository. This hierarchy prevents the circular dependencies that usually lead to those dreaded StackOverflowError loops during application startup.
📋 Practical Task
Refactor the Monolithic UserRegistrationController
You have been handed a legacy UserRegistrationController where the logic is completely tangled. Your task is to decompose this class into a proper layered architecture.
Current State:
The UserRegistrationController currently does the following in a single method:
1. Receives a UserDTO.
2. Manually checks if the email is already taken by calling jdbcTemplate.query(...).
3. Encrypts the password using a BCrypt utility.
4. Saves the user to the database using jdbcTemplate.update(...).
5. Returns a ResponseEntity.
Your Requirements:
- Create a
UserRepository: Move alljdbcTemplatecalls here. Create methods likeexistsByEmail(String email)andsave(User user). - Create a
UserService: Move the email validation logic and the password encryption logic here. The service should call the repository. - Refactor the
UserRegistrationController: Remove all database and encryption logic. It should now only calluserService.registerUser(dto)and return the appropriate HTTP response.
Success Criteria:
The Controller should have zero imports from java.sql.* or your database driver. The Repository should have zero imports from org.springframework.web.*.
There are no comments for now.