DEV Community

Twilight
Twilight

Posted on

Why Your Domain Entities Should Keep Their Own Secrets

Imagine you're at a party, and someone claims to be your friend Sarah's boyfriend. You've never met him before. To verify his identity, you have two options:

Scenario 1: The Wrong Way

You call Sarah and ask, "Hey, what's your Facebook password?"

Sarah reluctantly tells you, then you log into her account to check her relationship status.

This approach is problematic because:

  • Sarah had to reveal sensitive information
  • You now have access to information you shouldn't
  • The process required an uncomfortable breach of privacy

Scenario 2: The Right Way

You text Sarah: "There's a guy here claiming to be your boyfriend. Can you confirm?"

Sarah simply replies with a yes or no.

This is clearly better because:

  • Sarah's password remains private
  • You got the information you needed
  • The responsibility stayed with the right person

Back to Code

This everyday example perfectly illustrates what's happening in our API code:

The Wrong Approach

// Service layer peeks at private data
async login(credentials: LoginRequestDto): Promise<LoginResponseDto> {
  const user = await this.repository.findUserByEmail(email);

  // Breaking abstraction to get the password
  const rawUserData = await this.repository['prisma'].users.findUnique({
    where: { email }
  });

  // Checking the password ourselves
  if (!CryptoUtil.verifyPassword(
    credentials.password, 
    rawUserData.password, 
    rawUserData.salt
  )) {
    throw new UnauthorizedError();
  }
}
Enter fullscreen mode Exit fullscreen mode

The Better Approach

// User entity class
class UserEntity {
  private password: string;
  private salt: string;

  // The user keeps their own secrets
  verifyPassword(attemptedPassword: string): boolean {
    return CryptoUtil.verifyPassword(
      attemptedPassword, 
      this.password, 
      this.salt
    );
  }
}

// Service simply asks the user
async login(credentials: LoginRequestDto): Promise<LoginResponseDto> {
  const user = await this.repository.findUserByEmail(email);

  // Just like texting Sarah - we ask without seeing the secrets
  if (!user.verifyPassword(credentials.password)) {
    throw new UnauthorizedError();
  }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Benefits

This approach isn't just academically satisfying - it has practical benefits:

  1. Easier to maintain: When password hashing algorithms need updating, you change code in one place, not everywhere passwords are checked.

  2. Fewer bugs: The service layer doesn't need to know how passwords are stored or verified.

  3. Better security: Sensitive data stays encapsulated and isn't passed around unnecessarily.

  4. Cleaner code: Your authentication logic lives where it belongs.

Just like in real life, good boundaries make for better relationships. When each part of your system maintains responsibility for its own data, your code becomes more intuitive, maintainable, and secure.

The next time you're writing authentication logic, ask yourself: am I asking for someone's password, or am I simply asking them to verify something only they should know?

Top comments (0)