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
297: Hexagonal Architecture Concepts in Java
I want to show you something that usually happens about six months into a project. You've got a working app, everything is passing tests, and then the stakeholders drop a bomb: "We're moving from Postgres to MongoDB," or "We need to support both Stripe and PayPal for payments."
If you've built your app the way most tutorials teach—where the service layer calls the database repository directly—you're in for a miserable weekend of refactoring. Let's look at a typical "tightly coupled" payout system and see why it's a trap.
The "Everything-is-Connected" Mess
Here is how I often see Java services written. We have a PayoutService that handles sending money to a vendor. It looks clean at first glance, but look closely at the imports.
public class PayoutService {
private final PostgresPayoutRepository repository; // Concrete SQL implementation
private final StripePaymentGateway gateway; // Concrete Third-party API
public PayoutService(PostgresPayoutRepository repository, StripePaymentGateway gateway) {
this.repository = repository;
this.gateway = gateway;
}
public void processPayout(long payoutId, double amount) {
// Business logic lives here...
gateway.charge(amount);
repository.updateStatus(payoutId, "COMPLETED");
}
}
On the surface, it works. But the PayoutService—which should only care about the business rule of processing a payout—is now intimately aware of Postgres and Stripe. If I want to write a unit test for this service, I'm forced to mock these specific external libraries. If we switch to PayPal, I have to open this file and change the code. That's a violation of the Open/Closed Principle, and frankly, it's a maintenance nightmare.
The Moment it All Breaks
Let's pretend we just got the order to support PayPal. I try to add it to the existing service, and suddenly my constructor looks like this:
public PayoutService(PostgresPayoutRepository repo, StripeGateway stripe, PayPalGateway paypal, boolean usePayPal) {
// Now I'm writing if/else logic inside my business service just to handle infrastructure
}
I'm starting to feel the "friction." The business logic is being drowned out by plumbing. The PayoutService shouldn't care how the money is moved or where the record is saved. It should just know that a payout must be moved and a record must be saved.
Defining the Boundary
This is where we apply the Hexagonal (or "Ports and Adapters") concept. Instead of the service reaching out to the database, we define a "Port." In Java, a Port is simply an interface that lives inside the core business logic package.
I'll strip the concrete dependencies out and replace them with intentions:
// This is a Port. It's an "inside" definition.
public interface PayoutRepository {
void updateStatus(long id, String status);
}
// This is another Port.
public interface PaymentGateway {
void charge(double amount);
}
public class PayoutService {
private final PayoutRepository repository;
private final PaymentGateway gateway;
public PayoutService(PayoutRepository repository, PaymentGateway gateway) {
this.repository = repository;
this.gateway = gateway;
}
public void processPayout(long payoutId, double amount) {
gateway.charge(amount);
repository.updateStatus(payoutId, "COMPLETED");
}
}
Notice what happened? I've pushed the decision-making to the edge. The PayoutService is now "blind" to the outside world. It just says, "I need something that can charge money and something that can update a status."
Plugging in the Adapters
Now we need to actually make the code run. We create "Adapters." These are the concrete implementations that live outside the core. They wrap the messy details of SQL or REST APIs and translate them into the language our Port understands.
// Adapter for Postgres
public class PostgresPayoutAdapter implements PayoutRepository {
private final JdbcTemplate jdbcTemplate;
public void updateStatus(long id, String status) {
jdbcTemplate.update("UPDATE payouts SET status = ? WHERE id = ?", status, id);
}
}
// Adapter for Stripe
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient client;
public void charge(double amount) {
client.createCharge(amount);
}
}
The magic happens in the configuration (or your Dependency Injection framework). You decide which adapter to plug into the port. Want to switch to PayPal? You write a PayPalPaymentAdapter, swap one line in your config, and you don't touch a single line of code in your PayoutService.
That's the essence of Hexagonal Architecture: the core is a protected sanctuary of business rules, and everything else—databases, APIs, UIs—is just a pluggable detail.
📋 Practical Task
Refactoring the Legacy Notification Engine
You've inherited a NotificationService that is tightly coupled to a specific AWS SNS (Simple Notification Service) client. The business now wants the ability to send notifications via Email (SendGrid) or SMS (Twilio) depending on the user's preference, without changing the core notification logic.
Your Task:
- Create a
NotificationPortinterface that defines asend(String message, String recipient)method. - Refactor the
NotificationServiceso it depends on theNotificationPortinterface rather than a concrete AWS client. - Implement two adapters:
SnsNotificationAdapterandSendGridNotificationAdapter. - Ensure that the
NotificationServicecan process a notification without knowing which adapter is currently plugged in.
Constraint: The NotificationService class must not import any classes from the com.aws.sns or com.sendgrid packages.
There are no comments for now.