Single Responsibility & Advanatges in Compiled Languages

Single Responsibility & Advanatges in Compiled Languages

Learn how the Single Responsibility Principle supercharges compiled languages with faster builds, safer changes, and better maintainability.

When it comes to writing clean, maintainable code, few ideas shine as brightly as the Single Responsibility Principle (SRP). Part of the SOLID design principles, SRP states that a class or module should have only one reason to change, meaning it should stick to one job. This isn’t just a theoretical nicety; it’s a practical superpower, especially in backend development and even more so in compiled languages like Java or C++. Let’s break it down with a real-world example: user authentication in a web app.

The Tangled Mess: A UserService Gone Wild

Imagine you’re building a backend for a web application, and you need to authenticate users. Here’s a UserService class that tries to do it all:

class UserService {
  constructor() {
    this.database = new DatabaseConnection();
  }

  authenticateUser(email, password) {
    // Validate input
    if (!email || !password) {
      throw new Error("Email and password are required");
    }
    if (!email.includes("@")) {
      throw new Error("Invalid email format");
    }

    // Query database
    const user = this.database.query("SELECT * FROM users WHERE email = ?", [email]);
    if (!user) {
      throw new Error("User not found");
    }

    // Check password
    const isValid = bcrypt.compareSync(password, user.hashedPassword);
    if (!isValid) {
      throw new Error("Incorrect password");
    }

    // Generate JWT token
    const token = jwt.sign({ id: user.id, email }, "secret_key", { expiresIn: "1h" });
    return token;
  }
}

This class is a jack-of-all-trades: it validates input, talks to the database, checks passwords, and generates JWT tokens. At first glance, it might seem efficient, everything’s in one place! But here’s the catch: it’s a maintenance nightmare. If the database schema changes, or you need a stricter email validation rule, or the JWT library gets swapped out, you’re editing this one class for entirely unrelated reasons. It’s a ticking time bomb for bugs and frustration.

Refactoring with SRP: One Job, One Class

Let’s apply SRP and split this into focused components. Here’s the refactored version:

class UserValidator {
  validateCredentials(email, password) {
    if (!email || !password) {
      throw new Error("Email and password are required");
    }
    // Do not focus this, we are not going to implement this
    if (!email.includes("@")) {
      throw new Error("Invalid email format");
    }
  }
}

class UserRepository {
  constructor(database) {
    this.database = database;
  }

  findUserByEmail(email) {
    const user = this.database.query("SELECT * FROM users WHERE email = ?", [email]);
    if (!user) {
      throw new Error("User not found");
    }
    return user;
  }
}

class PasswordService {
  comparePassword(plainText, hashedPassword) {
    return bcrypt.compareSync(plainText, hashedPassword);
  }
}

class TokenService {
  generateJWT(userId, email) {
    return jwt.sign({ id: userId, email }, "secret_key", { expiresIn: "1h" });
  }
}

class UserService {
  constructor(validator, repo, passwordService, tokenService) {
    this.validator = validator;
    this.repo = repo;
    this.passwordService = passwordService;
    this.tokenService = tokenService;
  }

  authenticateUser(email, password) {
    this.validator.validateCredentials(email, password);
    const user = this.repo.findUserByEmail(email);
    const isValid = this.passwordService.comparePassword(password, user.hashedPassword);
    if (!isValid) {
      throw new Error("Incorrect password");
    }
    return this.tokenService.generateJWT(user.id, user.email);
  }
}

Now each class has one job:

  • UserValidator checks input.
  • UserRepository fetches user data.
  • PasswordService verifies passwords.
  • TokenService handles tokens.
  • UserService orchestrates the flow without doing the heavy lifting itself.

This is cleaner, but why does it matter so much more in compiled languages? Let’s dive into the advantages.

Why Compiled Languages Love SRP

In languages like Java, C++, or Rust, code gets compiled into machine code or bytecode before execution. SRP amplifies the benefits here in ways interpreted languages (like JavaScript or Python) don’t quite match. Here’s how:

  1. Selective Recompilation: Build Smarter, Not Harder
  • In a compiled language, say Java, each class lives in its own .java file. When you change PasswordService.java (e.g., to use a new hashing algorithm), only that file needs to recompile into PasswordService.class. Tools like Maven or Gradle detect this and skip recompiling UserService.class, UserRepository.class, etc. With the tangled version, any change—validation, database, or tokens—forces a full recompile of the monolithic UserService.java. In large projects, this saves serious build time.
  1. Smaller Patches: Deploy Less, Stress Less
  • After compilation, deploying updates becomes leaner. If you’re patching a server, you might only need to replace PasswordService.class in the .jar file, leaving the rest intact. In the tangled version, you’re redeploying the whole UserService.class even for a tiny tweak. For systems programming in C++, this could mean patching a single .o file instead of relinking an entire binary.
  1. Reduced Side Effects: Change with Confidence
  • SRP isolates responsibilities, so a change in one area won’t accidentally break another. Imagine adding a new validation rule in the tangled UserService. You might fat-finger a line and break the database query logic. With SRP, UserValidator is a separate entity—tweak it, recompile it, and UserRepository stays safe. This isolation is enforced at compile time, giving you a safety net interpreted languages lean on runtime to catch.

Beyond Compilation: A Universal Win

Even outside the compiled world, SRP shines. That reduced risk of side effects? It’s a lifesaver everywhere. In the refactored version, if TokenService fails to generate a JWT, it won’t crash your database calls in UserRepository. Debugging becomes a breeze—you know exactly where to look. Plus, teams can work on separate components without stepping on each other’s toes.

The Takeaway

The Single Responsibility Principle isn’t just a buzzword—it’s a practical tool for building better software. In compiled languages, it turbocharges your workflow with faster builds and leaner deployments. In our UserService example, splitting duties means a change to password logic doesn’t force a full rebuild or risk breaking unrelated code. Whether you’re slinging Java bytecode or C++ binaries, SRP keeps your code focused, your builds efficient, and your sanity intact.

So next time you’re tempted to cram everything into one class, pause. Give each piece its own job. Your future self—and your compiler—will thank you.


Album of the day: