Day 1 - Implementing the Room Entity in Awa

Olaoluwa Oke| 19 June 2025


There’s a concept in HCI called an interface metaphor. It’s when you design something in code that mimics how things work in real life, so people don’t have to re-learn everything from scratch

  • A name and Unique ID . (Most houses don’t, but this one needs to.).
  • An owner or overseer. Someone who started the space and carries a small sense of responsibility for it
  • Members. Those who share the space.
  • A key — in this case, an invite code. Something readable, not just a long random hash.
  • And finally, a timestamp — the moment the space came into existence.


  • That mental model turned into code. The ownerId points to the user who created the room. The memberIds list contains all participants. The inviteCode works like a key to get in , generated to be short and human-friendly.

    Technical Architecture — Room Entity Implementation


    1. Entity Definition

    Collection: rooms
    Fields:
  • id (String, primary key, auto-generated by MongoDB)
  • ownerId (String, references users.id)
  • memberIds (List <%String>, references users.id)
  • inviteCode (String, unique, 6-digits)
  • createdAt (Instant, auto-generated)


  • 2. Invite Code Utility

    fun generateInviteCode(length: Int = 6): String {
        val chars = ('a'..'z') + ('0'..'9')
        return (1..length).map { chars.random() }.joinToString("")
    }
    

    3. Repository Layer

  • Generates a random alphanumeric code of specified length.
  • Designed for readability and shareability (avoids special characters or uppercase).

  • interface RoomRepository : MongoRepository {
        fun findByInviteCode(inviteCode: String): Room?
    }
    


  • Extends MongoRepository<%Room, String>.
  • Adds findByInviteCode() for lookup during join operations.


  • 4. Service Layer — Join Room Flow

    joinRoom(userId: String, inviteCode: String)
    1. Fetch room via findByInviteCode(inviteCode).
    2. If no room found → throw RoomNotFoundException.
    3. Append userId to memberIds.
    4. Save updated room.
    5. Save updated room.

    After testing, I realized this wasn’t the smartest approach. 
    Keeping a memberIds array in the Room collection meant updating two collections for 
    every join operation(tight coupling). It was simpler 
    and more consistent to just give each User a roomId field and query membership from there.
    


    Problem With This Design
  • Fetch room via findByInviteCode(inviteCode).
  • Tight Coupling: Membership state lived inside both Room and User, forcing the system to maintain consistency across two collections.
  • Dual Writes: Any join operation required updating both entities in sequence, introducing the possibility of write skew or partial updates if one write failed.
  • SData Redundancy: Membership info was duplicated, violating single source of truth principles.
  • Query Complexity: Determining membership from the Room side meant scanning embedded arrays, which becomes inefficient as membership scales


  • fun joinRoom(userId: String, joinCode: String): UserResponse {
        val room = roomRepository.findByCode(joinCode)
            ?: throw NotFoundException("Room with code $joinCode not found")
    
        val user = userRepository.findById(ObjectId(userId))
            .orElseThrow { NotFoundException("User not found") }
    
        // already in a room?
        if (user.roomId != null) {
            throw AlreadyExistsException("User is already in a room")
        }
    
        // get users already in the room
        val usersInRoom = userRepository.findByRoomId(room.id!!)
        val roleToAssign = if (usersInRoom.isEmpty()) Role.OWNER else Role.MEMBER
    
        val updated = user.copy(
            roomId = room.id,
            role = roleToAssign
        )
        return userRepository.save(updated).toDTO()
    }
    
    

    6. Security Considerations
  • Invite codes are unique and short-lived in future versions (TTL index).
  • Only authenticated users can create or join rooms
  • Owner field is immutable post-creation.


  • Next step is to implement all the controller endpoints