Don’t Mix Different Levels of Abstraction — The Clean Code Approach
Don’t Mix Different Levels of Abstraction — The Clean Code Approach
Why shouldn’t mix different level of Abstraction while coding?

I remember the first time my senior engineer looked at my code during a code review. His exact words were: “This function is doing too many things at different levels. It’s like reading a recipe that jumps from ‘make dinner’ to ‘chop onions at 45-degree angles.’”
I was confused. The code worked perfectly. All tests passed. But he was right — my code was a mess of mixed abstraction levels, and I didn’t even know what that meant.
Today, I’m going to break down one of the most important principles from Robert C. Martin’s “Clean Code”: The Single Level of Abstraction Principle (SLAP). This principle has transformed how I write code, and more importantly, how I think about code organization.
What Are Levels of Abstraction?
Before we dive in, let’s understand what abstraction levels actually mean.
Think of abstraction levels like zoom levels on a map:
- High-level abstraction: “Travel from New York to Los Angeles”
- Medium-level abstraction: “Book a flight, get to airport, board plane”
- Low-level abstraction: “Enter credit card number, click submit button, print boarding pass”
In code, mixing these levels is like giving directions that say: “Drive to Los Angeles. But first, check your oil pressure sensor voltage and ensure tire pressure is exactly 32 PSI.”
Confusing, right? That’s what mixed abstraction levels do to your code.
The Problem: Mixed Abstraction Levels
Let me show you a real-world example. This is actual code I wrote early in my career (I’m not proud of it):
public void processUserRegistration(UserDTO userDTO) {
// High-level: Business logic
if (userDTO.getEmail() == null || userDTO.getEmail().isEmpty()) {
throw new ValidationException("Email is required");
}
// Low-level: String manipulation
String normalizedEmail = userDTO.getEmail().trim().toLowerCase();
if (!normalizedEmail.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new ValidationException("Invalid email format");
}
// Medium-level: Database operation
User existingUser = userRepository.findByEmail(normalizedEmail);
if (existingUser != null) {
throw new BusinessException("User already exists");
}
// Low-level: Password hashing
String salt = BCrypt.gensalt(12);
String hashedPassword = BCrypt.hashpw(userDTO.getPassword(), salt);
// High-level: Entity creation
User newUser = new User();
newUser.setEmail(normalizedEmail);
newUser.setPasswordHash(hashedPassword);
newUser.setCreatedAt(LocalDateTime.now());
// Medium-level: Persistence
userRepository.save(newUser);
// Low-level: Email sending
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(normalizedEmail);
helper.setSubject("Welcome to Our Platform");
helper.setText("Thank you for registering!");
mailSender.send(message);
// High-level: Analytics
analyticsService.trackUserRegistration(newUser.getId());
}
What’s wrong with this code?
Everything! This method jumps between:
- Business validation (high-level)
- String manipulation (low-level)
- Database queries (medium-level)
- Cryptographic operations (low-level)
- Email configuration (low-level)
- Analytics tracking (high-level)
Reading this code is exhausting. Your brain has to constantly switch context between different levels of detail.
The Solution: Single Level of Abstraction
Here’s the same code refactored to maintain a consistent abstraction level:
public void processUserRegistration(UserDTO userDTO) {
validateUserInput(userDTO);
ensureUserDoesNotExist(userDTO.getEmail());
User newUser = createUser(userDTO);
saveUser(newUser);
sendWelcomeEmail(newUser);
trackRegistration(newUser);
}
Now look at this. Every line is at the same level of abstraction. The method reads like a recipe:
- Validate input
- Check for duplicates
- Create user
- Save user
- Send email
- Track analytics
Each step is a meaningful operation at the same conceptual level. The implementation details are hidden in their respective methods.
The Complete Refactored Implementation
Here is more refactored solution:
// High-level orchestration
public void processUserRegistration(UserDTO userDTO) {
validateUserInput(userDTO);
ensureUserDoesNotExist(userDTO.getEmail());
User newUser = createUser(userDTO);
saveUser(newUser);
sendWelcomeEmail(newUser);
trackRegistration(newUser);
}
// Medium-level: Validation logic
private void validateUserInput(UserDTO userDTO) {
emailValidator.validate(userDTO.getEmail());
passwordValidator.validate(userDTO.getPassword());
}
private void ensureUserDoesNotExist(String email) {
if (userRepository.existsByEmail(email)) {
throw new BusinessException("User already exists");
}
}
// Medium-level: User creation
private User createUser(UserDTO userDTO) {
String normalizedEmail = emailNormalizer.normalize(userDTO.getEmail());
String hashedPassword = passwordHasher.hash(userDTO.getPassword());
return User.builder()
.email(normalizedEmail)
.passwordHash(hashedPassword)
.createdAt(LocalDateTime.now())
.build();
}
// Medium-level: Operations
private void saveUser(User user) {
userRepository.save(user);
}
private void sendWelcomeEmail(User user) {
emailService.sendWelcomeEmail(user.getEmail());
}
private void trackRegistration(User user) {
analyticsService.trackUserRegistration(user.getId());
}
Supporting Classes (Lower Levels)
Here are low level supporting classes:
// Low-level: Email validation
public class EmailValidator {
private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$";
public void validate(String email) {
if (email == null || email.isEmpty()) {
throw new ValidationException("Email is required");
}
if (!email.matches(EMAIL_REGEX)) {
throw new ValidationException("Invalid email format");
}
}
}
// Low-level: Email normalization
public class EmailNormalizer {
public String normalize(String email) {
return email.trim().toLowerCase();
}
}
// Low-level: Password hashing
public class PasswordHasher {
private static final int BCRYPT_ROUNDS = 12;
public String hash(String password) {
String salt = BCrypt.gensalt(BCRYPT_ROUNDS);
return BCrypt.hashpw(password, salt);
}
}
The Benefits Are Immediate
After refactoring, here’s what changed:
1. Readability Improved Dramatically
The main method now reads like documentation. Anyone can understand the registration flow without diving into implementation details.
2. Testing Became Easier
Look at these test cases, how easy are they now become to write:
@Test
public void shouldValidateEmailFormat() {
// Test only email validation
emailValidator.validate("invalid-email");
// No need to set up database, email service, etc.
}
@Test
public void shouldHashPasswordCorrectly() {
// Test only password hashing
String hashed = passwordHasher.hash("password123");
assertTrue(BCrypt.checkpw("password123", hashed));
}
Each component can be tested in isolation. No more massive integration tests for simple validation logic.
3. Maintenance Became Trivial
Want to change the email regex? Update EmailValidator. Want to switch from BCrypt to Argon2? Update PasswordHasher. Want to add rate limiting? Add a single line in the main method.
4. Code Reuse Increased
Now EmailValidator can be used anywhere in the application. So can PasswordHasher and EmailNormalizer. The mixed abstraction version? Impossible to reuse.
Real-World Examples of Mixed Abstractions
Let me show you more examples I’ve encountered in production code:
Bad: Payment Processing
public void processPayment(Order order) {
// High-level business logic
if (order.getTotal() <= 0) {
throw new InvalidOrderException("Order total must be positive");
}
// Low-level HTTP details
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://payment-gateway.com/api/charge"))
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(
"{"amount":" + order.getTotal() + ","currency":"USD"}"
))
.build();
// Low-level response parsing
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
JSONObject json = new JSONObject(response.body());
// High-level business logic
if (json.getString("status").equals("success")) {
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
emailService.sendReceipt(order.getCustomerEmail());
}
}
Good: Payment Processing
public void processPayment(Order order) {
validateOrder(order);
PaymentResult result = paymentGateway.charge(order.getTotal());
if (result.isSuccessful()) {
markOrderAsPaid(order);
sendReceipt(order);
}
}
How to Identify Mixed Abstraction Levels?
This is one skill senior developer should master and this is also tested now on coding interviews.
Here are the warning signs I look for during code reviews:
1. The “And Then” Test
If you’re describing a method and using “and then” more than twice, you probably have mixed abstractions.
“This method validates the email and then it opens an SMTP connection and then it updates the database and then…”
2. Different Comment Styles
// Business rule: Users must be 18+
if (user.getAge() < 18) { ... }
// Parse JSON response
JSONObject obj = new JSONObject(response);
String status = obj.getString("status");
If your comments jump from business concepts to technical details, you’re mixing levels.
3. Nested Loops with Business Logic
for (Order order : orders) {
for (Item item : order.getItems()) {
// Suddenly: complex business rule
if (item.isDiscountEligible() && !customer.hasUsedDiscount()) {
// And now: string manipulation
String code = "DISC" + UUID.randomUUID().toString().substring(0, 8);
// ...
}
}
}
4. The Import Test
If a single class imports both java.sql.Connection and high-level business interfaces, you’re probably mixing abstractions.
The Stepdown Rule
Robert Martin introduces the Stepdown Rule in Clean Code:
“We want the code to read like a top-down narrative. We want every function to be followed by those at the next level of abstraction.”
Think of it like reading a newspaper:
- Headline (highest level)
- Summary paragraph
- Detailed explanation
- Technical specifics
Your code should follow the same pattern:
// Level 1: Headline
public void generateReport() {
collectData();
processData();
formatReport();
saveReport();
}
// Level 2: Summary
private void collectData() {
fetchUserData();
fetchTransactionData();
fetchAnalytics();
}
// Level 3: Details
private void fetchUserData() {
List<User> users = userRepository.findAll();
this.userData = users;
}
// Level 4: Technical specifics (if needed)
// In UserRepository implementation...
Here is how it looks:

Catching These Issues Early with AI Code Review
Here’s the reality: Even after years of experience, I still occasionally mix abstraction levels. It’s easy to do when you’re focused on making something work.
That’s where AI-powered code review tools like CodeRabbit come in handy.
Why CodeRabbit?
I started using CodeRabbit a few months ago, and it’s been a game-changer for catching abstraction issues early. Here’s what impressed me:
1. It Catches Mixed Abstractions Automatically
CodeRabbit analyzes your pull requests and flags methods that mix different levels of abstraction. Instead of waiting for a senior developer to catch it in review, you get immediate feedback.
For example, it would flag this code:
public void processOrder(Order order) {
// High-level
validateOrder(order);
// Low-level - CodeRabbit would flag this
String sql = "INSERT INTO orders (id, total) VALUES (?, ?)";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setLong(1, order.getId());
stmt.setDouble(2, order.getTotal());
stmt.executeUpdate();
// High-level
sendConfirmation(order);
}
CodeRabbit’s suggestion: “Consider extracting the database operation into a repository method to maintain consistent abstraction levels.”
2. Learning Opportunity
Each suggestion comes with an explanation. Over time, you start recognizing these patterns yourself. It’s like having a mentor reviewing every line of code.
3. Works in Your IDE
With the CodeRabbit CLI, you get feedback before even committing your code. Catch issues while they’re still in your local environment.
4. Improves Team Consistency
When everyone on the team uses CodeRabbit, you develop a shared understanding of code quality. New team members learn clean code principles faster.
My CodeRabbit Workflow
Here’s how I integrate CodeRabbit into my daily development:
- Write code with focus on functionality
- Run CodeRabbit CLI locally to catch obvious issues
- Refactor based on suggestions
- Create pull request — CodeRabbit reviews it automatically
- Address remaining feedback before human review
- Learn from patterns — I keep notes on common issues
The result? My code reviews now focus on architecture and business logic, not basic clean code principles.
Common Objections (And My Responses)
Nothing is perfect and different people will see things differently:
“Isn’t this over-engineering?”
No. Over-engineering is adding complexity you don’t need. Separating abstraction levels reduces complexity by making each piece simpler.
Compare:
- One 200-line method (simple count, but complex to understand)
- Ten 20-line methods (more files, but each is trivial to understand)
The second is easier to maintain, test, and modify.
“It’s just adding more methods!”
Yes, and that’s good. More small, focused methods are easier to understand than fewer large methods.
Would you rather read:
- One 50-page document about “Everything About Our Company”
- Ten 5-page documents, each focused on one topic
Small, focused units are always easier to digest.
“My team won’t understand this”
They will, once they experience it. I’ve introduced this principle to three different teams. Initial resistance lasted about two weeks. Then:
“Wait, I can test this validator in isolation? This is amazing!” “I found the bug in 2 minutes because each method does one thing!” “New developer onboarded in 3 days instead of 3 weeks!”
The benefits speak for themselves.
“It takes more time to write”
Initially, yes. Long-term, no. You spend 15 minutes refactoring now to save 2 hours of debugging later. Over the lifetime of a project, clean code is much faster to work with.
Plus, with tools like CodeRabbit, you catch these issues early and learn the patterns faster.
Practical Exercise: Refactor This Code
Here’s an exercise. Try refactoring this code to maintain single abstraction levels:
public void exportCustomerData(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));
StringBuilder csv = new StringBuilder();
csv.append("Name,Email,Total Spentn");
csv.append(customer.getName()).append(",");
csv.append(customer.getEmail()).append(",");
List<Order> orders = orderRepository.findByCustomerId(customerId);
double total = 0;
for (Order order : orders) {
total += order.getTotal();
}
csv.append(String.format("%.2f", total));
Path filePath = Paths.get("exports", "customer_" + customerId + ".csv");
Files.createDirectories(filePath.getParent());
Files.write(filePath, csv.toString().getBytes());
System.out.println("Export completed: " + filePath);
}
My Refactored Solution
Don’t look until you try yourself:
// High-level orchestration
public void exportCustomerData(Long customerId) {
Customer customer = fetchCustomer(customerId);
CustomerExport export = prepareExport(customer);
saveExport(export);
logSuccess(export);
}
// Medium-level operations
private Customer fetchCustomer(Long customerId) {
return customerRepository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));
}
private CustomerExport prepareExport(Customer customer) {
double totalSpent = calculateTotalSpent(customer.getId());
return new CustomerExport(customer, totalSpent);
}
private double calculateTotalSpent(Long customerId) {
return orderRepository.findByCustomerId(customerId)
.stream()
.mapToDouble(Order::getTotal)
.sum();
}
private void saveExport(CustomerExport export) {
String csvContent = csvFormatter.format(export);
fileService.saveToExports(export.getFileName(), csvContent);
}
private void logSuccess(CustomerExport export) {
logger.info("Export completed: {}", export.getFilePath());
}
Supporting classes handle the low-level details:
- CsvFormatter handles CSV generation
- FileService handles file operations
- Each class has a single responsibility
The Clean Code Checklist
Before submitting your code for review, ask yourself:
- Can I describe this method without using “and then”?
- Are all operations at the same conceptual level?
- Could each step be explained to a non-programmer?
- Are low-level details hidden in helper methods?
- Does the method read like a story?
- Would CodeRabbit flag any mixed abstractions?
If you answer “yes” to all, you’re writing clean code.
Conclusion: Start Small, Build Habits
You don’t need to refactor your entire codebase tomorrow. Start small:
- This week: Refactor one method that mixes abstractions
- Next week: Apply SLAP to all new code you write
- Next month: Review and refactor one class per day
- Ongoing: Use CodeRabbit to catch issues early and learn patterns
The Single Level of Abstraction Principle transformed how I write code. It will do the same for you.
Clean code isn’t about being clever. It’s about being clear. It’s about making your code so obvious that the next developer (who might be you in six months) can understand and modify it without fear.
And with AI tools like CodeRabbit catching these issues automatically, there’s no excuse for mixed abstraction levels in modern codebases.
Ready to write cleaner code?
- Try CodeRabbit for automated code review
- Get instant feedback with the CodeRabbit CLI
- Start applying SLAP to your code today
What’s the worst mixed abstraction code you’ve encountered? Share your horror stories in the comments below!
Further Reading
- Clean Code by Robert C. Martin (The bible on this topic)
- The Art of Readable Code by Dustin Boswell
- Code Complete by Steve McConnell
Remember: Clean code is not about being perfect. It’s about being better than yesterday.
Don’t Mix Different Levels of Abstraction — The Clean Code Approach was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More

