Skip to main content

Abstraction in Java

Core Understanding

Abstraction is about hiding complex implementation details and showing only necessary features of an object:

  • Reduces complexity by hiding unnecessary details
  • Provides high-level interfaces for interaction
  • Separates interface from implementation
  • Enables modular design
  • Supports loose coupling between components

❌ Bad Example

public class ReportGenerator {
public byte[] generateReport(String reportData) {
// Direct low-level implementation without abstraction
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM data");

// Direct file handling
File file = new File("report.pdf");
PDDocument document = new PDDocument();
PDPage page = new PDPage();
document.addPage(page);

// Direct email sending
Session session = Session.getInstance(new Properties());
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress("sender@example.com"));

// Mix of different concerns without abstraction
while (rs.next()) {
// Complex PDF generation logic
// Mixed with data access and email logic
}

document.save(file);
Transport.send(message);
return Files.readAllBytes(file.toPath());
}
}

Why it's bad:

  • No separation of concerns
  • Direct coupling to implementation details
  • Hard to test and maintain
  • No abstraction layers
  • Mixed responsibilities

✅ Good Example

Let's fix this:

public interface ReportGenerator {
Report generateReport(ReportRequest request);
}

public interface ReportStorage {
void store(Report report);
Optional<Report> retrieve(String reportId);
}

public interface ReportNotifier {
void notifyReportCompletion(Report report);
}

@Service
public class PDFReportGenerator implements ReportGenerator {
private final DataSource dataSource;
private final ReportTemplate template;
private final PDFEngine pdfEngine;

@Override
public Report generateReport(ReportRequest request) {
List<ReportData> data = dataSource.fetchData(request.getQuery());
Document document = template.applyData(data);
byte[] pdfContent = pdfEngine.generate(document);

return new Report(
request.getReportId(),
pdfContent,
ReportFormat.PDF,
LocalDateTime.now()
);
}
}

@Service
public class ReportingService {
private final ReportGenerator generator;
private final ReportStorage storage;
private final ReportNotifier notifier;

public ReportResult createReport(ReportRequest request) {
try {
Report report = generator.generateReport(request);
storage.store(report);
notifier.notifyReportCompletion(report);

return ReportResult.success(report.getId());
} catch (Exception e) {
return ReportResult.failure("Failed to generate report: " + e.getMessage());
}
}
}

Why it's good:

  • Clear separation of concerns
  • Well-defined interfaces
  • Hidden implementation details
  • Easy to test and modify
  • Loose coupling between components

Best Practices

  • Use Interface Segregation
// Split interfaces based on client needs
public interface Readable {
byte[] read();
}

public interface Writable {
void write(byte[] data);
}

public interface Closeable {
void close();
}

// Combine when needed
public interface FileOperation extends Readable, Writable, Closeable {
void sync();
}
  • Abstract Factory Pattern
public interface ConnectionFactory {
Connection createConnection(ConnectionConfig config);
}

@Service
public class DatabaseConnectionFactory implements ConnectionFactory {
@Override
public Connection createConnection(ConnectionConfig config) {
return switch(config.getType()) {
case POSTGRES -> createPostgresConnection(config);
case MYSQL -> createMysqlConnection(config);
default -> throw new UnsupportedDatabaseException(config.getType());
};
}
}
  • Layer Abstraction
public interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
}

@Repository
public class JpaUserRepository implements UserRepository {
private final JpaRepository<UserEntity, Long> jpaRepository;
private final UserMapper mapper;

@Override
public Optional<User> findById(Long id) {
return jpaRepository.findById(id)
.map(mapper::toDomain);
}
}

Use Cases

  • Database Access

    • Repository pattern
    • Data access objects
    • Connection pooling
  • External Services

    • API clients
    • Message queues
    • Cache implementations
  • Business Logic

    • Service layer
    • Domain models
    • Validation rules

Anti-patterns to Avoid

  • Leaky Abstraction
// Avoid exposing implementation details
public interface UserService {
// Don't expose SQL!
List<User> findByCustomQuery(String sqlQuery);

// Better
List<User> findByAttributes(UserSearchCriteria criteria);
}
  • Concrete Class Dependencies
// Avoid
public class OrderService {
private MySQLOrderRepository repository; // Concrete class

// Better
private OrderRepository repository; // Interface
}
  • Mixed Abstraction Levels
// Avoid mixing abstraction levels
public interface ReportService {
Report generateReport(); // High-level
void connectToDatabase(); // Low-level
byte[] convertToPdf(); // Low-level
}

Interview Questions & Answers

Q1: "How do you handle abstraction in distributed systems?"

Answer:

public interface RemoteService {
// High-level abstraction for remote operations
<T> CompletableFuture<T> executeRemote(RemoteCommand<T> command);

// Circuit breaker abstraction
interface CircuitBreaker {
<T> CompletableFuture<T> execute(Supplier<CompletableFuture<T>> command);
}
}

@Service
public class RemoteServiceImpl implements RemoteService {
private final CircuitBreaker circuitBreaker;
private final RetryTemplate retryTemplate;

@Override
public <T> CompletableFuture<T> executeRemote(RemoteCommand<T> command) {
return circuitBreaker.execute(() ->
retryTemplate.executeAsync(() ->
command.execute()
)
);
}
}