Day 2 (Part 1) - User Entity, Password Encoder , BcryptPasswordEncoder

Olaoluwa Oke| 14 June 2023

I think I must’ve skipped over the User entity in my earlier notes. That’s probably because I wrote it on Day One — fast. Mostly because I had already written something nearly identical for my bookmarking app, Tabitha.The style carried over nicely. And with the ERD already sketched out, this really wasn’t uncharted territory.

The model is intentionally plain: identity, contact, auth, membership, lifecycle.

Auth is next on the runway, so I wanted the substrate ready: stable fields, predictable lookups, and password handling that won’t make future-me wince.



The User entity is pretty standard:


Technical Log

(1) Domain Model
id firstName email password roomid role joinedAt isActive

enum class Role { OWNER, MEMBER }

2. Repository Layer


interface UserRepository : MongoRepository<%User, String> {
    fun findByEmailIgnoreCase(email: String): User?
    fun findByRoomId(roomId: String): List<%User>
}


  • findByEmailIgnoreCase backs login flows and signup duplicate checks.
  • findByRoomId powers room membership views and role computations (e.g., first joiner = OWNER).


  • (3) Password Handling (BCrypt)

  • Store only one-way hashes (BCrypt). No reversible encoding, no salts managed by you—BCrypt handles per-hash salts and cost factor..
  • Minimal API surface in a PasswordService:
  • encode(raw: String): String
    matches(raw: String, hash: String): Boolean

    Rationale :
  • Resistant to rainbow tables and supports work factor tuning (cost) as hardware improves.
  • Keeps auth concerns cohesive and testable, separate from controllers/services.


  • (4) DTOs (Auth-Adjacent)


  • UserCreateRequest(firstName, email, password) → input
  • UserResponse(id, firstName, email, roomId, role, joinedAt, isActive) → output
  • (Auth-specific DTOs will arrive with the auth feature: LoginRequest, AuthResponse w/ JWT.)

  • Principles
  • Prevent overexposure of internals (never leak password/hash).
  • Keep API contracts stable while entities evolve.


  • (5) Service Layer (UserService)

    Injected deps: UserRepository, RoomRepository (see note below).
  • createUser(req: UserCreateRequest): UserResponse
  • Lowercase + validate email, enforce uniqueness, password = bcrypt.encode(...), default isActive = true, role = MEMBER, joinedAt = now()
  • deactivateUser(userId): Unit → sets isActive = false (idempotent).
  • updateUser(userId, patch): UserResponse → constrained fields (no direct role escalations here).
  • getUsersInRoom(roomId): List<%UserResponse>
  • joinRoom(userId, code): UserResponse
  • Guards: user not already in a room; room exists; assign Role.OWNER if first member else MEMBER; single write to User (no dual writes)
    Uses userRepository.findByRoomId(room.id) to determine role.

    Why inject RoomRepository here?
  • The join flow needs to resolve and validate the target room by code/ID and may apply future room-level invariants (e.g., capacity, status, invite TTL). Keeping that lookup centralized in the service avoids controller-level anemic logic and keeps business rules cohesive.

  • More in the next entry when auth & room constraints kick in.


  • (6) Validation & Errors

  • Email format + normalization (lowercase).
  • Unique email → map duplicate key to 409.
  • Not found → 404; already in a room → 409.
  • Centralized via @ControllerAdvice returning a compact error payload.


  • (7) Security Posture (pre-auth)

  • Never return password or security-adjacent fields in responses.
  • Start failing closed: endpoints require auth once JWT lands; until then, keep mutation endpoints behind a temporary gate (e.g., dev profile).
  • ERD image