Decision
BestBuds should be refactored around a small set of powerful concepts: graph, route, interactor, actor, repository, service, screen state, and scoped design component. Screens should be declarative and readable. Interactors should own data plus actions. Actors should own focused asynchronous loops and state machines. The design module should expose a proper atomic hierarchy without BB* wrapper compatibility shims.
Current Shape
The app has already moved a long way from the legacy frontend. The remaining issue is not lack of components; it is mixed abstraction levels. Some screens still combine graph payloads, repository calls, data shaping, analytics, mutable local workflow state, layout, and low-level visual details in the same file. Some components are well-scoped; others are just moved elsewhere, which creates ravioli code instead of declarative UI.
| Hotspot | Observed shape | Refactor pressure |
|---|---|---|
ChatInteractor.kt | Roughly 800+ lines. Owns chat list, open/close, cache refresh, WebSocket lifecycle, optimistic sends, failures, participants, typing, presence, reactions, forwarding, and pagination. | Split into a parent session interactor plus focused actors. This is the highest leverage refactor. |
ChatScreen.kt | Collects many flows, owns reply/highlight/photo/action-sheet/reactor/forwarding state, derives subtitle, triggers pagination, and declares UI. | Move transient workflows into ChatScreenState and action APIs. Keep the visible skeleton in the screen. |
ChatsScreen.kt | Close to the desired style, but still hydrates stickers, filters chats, logs analytics, and opens routes directly inside screen composition. | Extract ChatListInteractor while preserving a readable screen skeleton. |
ProfileScreen.kt | Mixes profile fetch, editor fields, dirty detection, session state, geo side effects, analytics, logout, and UI model shaping. | Create ProfileInteractor and ProfileEditorActor. |
OnboardingScreen.kt | Owns questions, step state, responses, timings, analytics, submission, completion, and route decisions. | Create OnboardingInteractor with flow actor and submission actor. |
PublicEventsScreen.kt | Uses sample data and local save state. Search/filter UI is good, but event data and actions are not graph-owned. | Create EventsInteractor and wire create/open/save/join as real actions. |
modules/design | Strong visual foundation, but component contracts are broad and some files are large or implementation-oriented. | Introduce atomic folders and narrower capability scopes under themed {}. |
Target Model
The target structure is a graph-backed UI architecture where each layer has a precise reason to exist. The goal is not ceremony. The goal is that a small number of concepts can express a large product surface without duplicating logic or leaking implementation details upward.
| Layer | Owns | Must not own | Example |
|---|---|---|---|
| Service | Typed transport contracts and request/response shapes. | Screen state, app labels, UI concepts, cache policy, or operation names for HTTP handlers. | MessagingApi, SocialApi, StickerApi. |
| Repository | Persistence, cache, local source of truth, and remote fetch wrappers. | Screen composition, analytics, navigation, or transient UI workflow. | UserRepository, MessageRepository, SocialRepository. |
| Interactor | Data plus actions. Combines repositories, services, analytics, and route-safe domain operations. | Low-level Compose layout or primitive style choices. | ChatListInteractor, ProfileInteractor, EventsInteractor. |
| Actor | One focused async loop, reducer, queue, or state machine. | Multiple unrelated workflows or broad product orchestration. | ChatSocketActor, ComposerActor, OnboardingSubmissionActor. |
| Screen state | Immutable state model shaped for UI rendering. | Networking, mutation logic, or Compose dependencies. | ChatScreenState, ProfileEditorState. |
| Screen | Route binding, state collection, action forwarding, and context-appropriate declarative layout. | Repository calls, service clients, analytics details, data hydration, or ad hoc business rules. | ChatScreen.Content() reads like the actual screen. |
| Design component | Reusable rendering and interaction primitives at the right atomic level. | Product data fetching or route behavior. | Card, ConversationRow, MessageTimeline, ProfileHeader. |
edge.push(payload) is the reference style. It hides incidental boilerplate while preserving the real concept. The same standard should apply to UI and data APIs.
Atomic Design
Atomic design gives us a vocabulary for moving between parts and whole without losing either. Brad Frost's model uses atoms, molecules, organisms, templates, and pages. For BestBuds, the names should be adapted to Compose and product code, but the hierarchy should remain strict.
| Atomic stage | BestBuds meaning | Examples | Rules |
|---|---|---|---|
| Tokens | Subatomic design constants and semantic decisions. | Color palette, typography, spacing, corners, elevation, motion durations. | Never encode screen-specific alpha, color, or spacing in a product screen when a semantic token exists. |
| Primitives / atoms | Smallest functional UI elements. | Text, IconAction, Card, Button, Avatar, TextField, Badge. | No repository, no product workflow, minimal styling branches, stable accessibility contract. |
| Molecules | Small reusable groups with one purpose. | SearchBar, PillGroup, StatRow, ProfileFact, ComposerActionStrip. | Use atoms and tokens. Do not become a full screen section. |
| Organisms | Distinct product sections composed from molecules and atoms. | ConversationRow, MessageTimeline, ProfileHeader, EventCard, CampaignStageTrack. | Can know product UI models, but not repositories or network behavior. |
| Screen blocks | Named screen-level regions where reuse is real and the abstraction helps reading. | ChatHeader, ComposerDock, OnboardingQuestionStage, ProfileEditorSection. | Extract only when the name improves the screen's readability. Avoid one-line template indirection. |
| Screens / pages | Route-bound concrete UI instances with real content and actions. | ChatScreen, PublicEventsScreen, ProfileScreen. | Should reveal the visual skeleton directly: top bar, list, header, sections, sheets, dock, FAB. |
React/Actor Philosophy
The product should follow the same mental model that makes React and actor systems powerful: a small number of composable primitives, explicit state, clear message flow, and local reasoning. The implementation should not grow new concepts unless they generalize across real use cases.
React side
UI is a function of state. Components are composed, not inherited. A screen should read top-down. Props and action callbacks should make the data flow visible. Styling should come from scoped theme contracts, not random local constants.
Actor side
Async workflows are isolated. A socket actor owns socket state. A composer actor owns draft, reply, attachments, and send lifecycle. A submission actor owns validation and remote mutation. State changes happen through explicit actions.
What this means in code
- A screen should call
interactor.actions.send(), not assemble service payloads. - A screen should render
state.timeline, not fetch, merge, sort, and annotate messages. - A component should render a
ConversationSummaryUi, not inspect raw service DTOs. - An actor should expose a focused state flow and action surface, not a broad utility object.
- New primitives must serve multiple domains or be clearly local to one feature.
Graph Ownership
Reaktor's graph model should be the backbone of the app, not just a navigation wrapper. BestBuds should use graph nodes to make lifecycle and ownership explicit: services and repositories are providers, interactors are product capability nodes, actors are owned by interactors or narrow graph nodes, and screens consume only the state/actions they need.
| Graph object | Recommended role | BestBuds application |
|---|---|---|
BestBuds | Readable app graph and route map. It can be longer than 500 lines if it remains the single obvious app topology. | Declare providers, top-level interactors, routes, containers, and edge bindings. Avoid business logic. |
ServiceNode | Typed API provider for real services. | Expose MessagingApi, SocialApi, and StickerApi. No ad hoc client service labels. |
RepositoryNode | Offline-first data access and cache lifecycle. | User, message, sticker, social, and profile data should live here. |
Interactor | Product behavior boundary. | Chat, events, campaigns, friends, onboarding, profile, palette/session. |
ActorNode | Focused event loop where ordering matters. | Socket, composer, forwarding, onboarding submission, media upload, profile save. |
RouteBinding | Route payload and navigation actions. | Use edge.push(payload), edge.replace(payload), and correct return/back commands. |
Screen Inventory
The current UI is visually stronger than before, but data and action ownership varies by screen. The refactor should be comprehensive and staged by product flow risk.
| Screen group | Current concern | Target screen shape | Interactor target |
|---|---|---|---|
| Start / auth | Login and quick test-user paths are close to screen-level orchestration. | Start screen declares hero, sign-in actions, trust/value blocks, and optional dev impersonation block. | AuthInteractor or SessionInteractor. |
| Onboarding | Questions, local responses, timers, analytics, validation, and submission are mixed into the screen. | Screen renders progress, question stage, answer controls, and bottom action dock. | OnboardingInteractor with OnboardingFlowActor and OnboardingSubmissionActor. |
| Chat list | Filtering, hydration, unread counts, pinned sections, analytics, and navigation are still local. | Screen declares hero, filter bar, pinned section, active circles, friends, empty states. | ChatListInteractor. |
| Chat thread | Large flow collection and transient UI state in the screen; large interactor behind it. | Screen declares scaffold, header, timeline, sheets, member rail, composer dock, and error banners. | ChatSessionInteractor with focused actors. |
| Group profile | Beautiful direction, but group context, members, events, safety, and reveal are not fully state-owned. | Screen declares identity hero, stage track, members, pinned plan, memory wall, safety controls. | GroupProfileInteractor. |
| Personal profile | Fetch, edit fields, save state, session geo, logout, analytics, and UI shaping are mixed. | Screen declares profile hero, editor sections, privacy block, linked accounts, logout action. | ProfileInteractor and ProfileEditorActor. |
| Friend profile | Direct repository data and simple profile display. | Screen declares friend hero, prompts, mutual circles, shared events, plan/message actions. | FriendProfileInteractor. |
| Campaigns / discover | Strong cards, shallow actions, no detail depth. | Screen declares stage summary, recommendation explanation, campaign sections, detail entry. | CampaignInteractor and DiscoverInteractor. |
| Public/private events | Sample data and local save state; create/open/join not real enough. | Screen declares event hero, filters, event feed, create FAB, detail sheets/routes. | EventsInteractor. |
| Friends | Simple list with profile routing. | Screen declares search, close friends, recent activity, groups, plans, empty recovery. | FriendsInteractor. |
| Palette / dev | Useful but release-sensitive. | Palette stays as design tool behind dropdown/dev access. Dev screens are hidden or gated in release builds. | PaletteInteractor or theme controller. |
Domain Interactors
Interactors should become the product API used by screens. The intent is not to make every action global. The intent is that each domain has one obvious place where data, actions, analytics, cache invalidation, optimistic updates, and navigation-safe product decisions live.
Chat domain
ChatListInteractor: fetch summaries, hydrate stickers, derive sections, search/filter, unread counts, pinned groups, analytics for open.ChatSessionInteractor: load one thread, exposeChatScreenState, coordinate timeline, participants, composer, sheets, and route actions.ChatSocketActor: connect, reconnect, receive, send, close, online state, typing receive.MessageTimelineActor: pages, merge, sort, dedupe, highlight, scroll triggers, optimistic replacement.ComposerActor: draft, reply target, attachments, sticker picker, validation, send lifecycle.ReactionActor: reaction sheets, quick reactions, optimistic toggles, undo.ForwardingActor: select targets, confirm, send copies, dismiss.
Identity and trust
SessionInteractor: current user, login/logout, test persona gating, session readiness.OnboardingInteractor: question state, responses, progress, validation, submission, completion routing.ProfileInteractor: fetch, edit state, dirty state, save, privacy, media, logout handoff.FriendProfileInteractor: friend context, shared history, message/plan actions.GroupProfileInteractor: members, stage, reveal, pinned plan, events, safety controls.
IRL loop
EventsInteractor: public/private feeds, filters, saved events, RSVP, create, open detail, empty states.CreateEventInteractor: editor state, image selection, validation, publish, drafts.CampaignInteractor: current campaign, stage status, members, trust ring, action readiness.DiscoverInteractor: recommendations, search, local filters, explanation, join/request flows.
Shell and theme
NavigationInteractor: route-visible tab metadata, unread badges, shell actions.PaletteInteractor: palette selection, preview, persistence, system theme adaptation.DevInteractor: debug-only toggles and diagnostics hidden from release builds.
state: StateFlow<ScreenState> plus small action functions. Avoid mutable public fields and avoid exposing raw repository flows directly to screens.
UI Layering
The design module should keep moving toward a real atomic library. App screens should consume semantic organisms and screen blocks, while still showing enough structure to be readable. The themed {} scope should be the primary way to access theme-aware components.
| Package | Content | Allowed dependencies | Forbidden dependencies |
|---|---|---|---|
design.tokens | Color, typography, spacing, corners, motion, elevations. | Kotlin/Compose primitives. | App models, screens, repositories. |
design.primitives | Foundational UI elements: surface, text, icon action, image, input, button. | Tokens and Compose primitives. | Product models, network state. |
design.atoms | Badges, pills, stats, labels, dividers, avatars, menu items. | Primitives and tokens. | Screen-specific branching. |
design.molecules | Search bars, action rows, input groups, summary rows, media pickers. | Atoms and primitives. | Repository calls, navigation. |
design.organisms | Conversation rows, event cards, profile headers, message bubbles, composer dock. | Molecules, atoms, UI models. | Services, repositories, graph dispatch. |
app.ui.* | Route screens and private screen blocks. | Design organisms, interactors, route binding. | Service clients, broad data manipulation. |
Scope facets
BestBudsThemeScope can be powerful without becoming a single huge interface. Prefer capability facets installed into the themed scope:
SurfaceComponents: card, panel, sheet, backdrop, separators.TextComponents: headings, body copy, captions, labels, metadata.InputComponents: text fields, search, composer input, segmented controls, chips.IdentityComponents: avatars, stacks, identity rows, verification marks.FeedComponents: list sections, empty states, loading states, row containers.ActionComponents: buttons, icon actions, floating actions, menus, action rows.MotionComponents: transitions, press feedback, reveal, shimmer, scroll effects.
Component Rules
Component extraction should make the code more expressive. It should never hide the screen's structure or create a thin wrapper around a primitive just to add a prefix.
Examples
These examples show the target level of abstraction. They are not final code, but they define the shape of the refactor.
Chat list screen after refactor
@Composable
override fun Content() = themed {
TrackScreenView("chat_list")
val state by chatListInteractor.state.collectAsState()
ScreenColumn(
contentPadding = ScreenPadding.feed,
verticalSpacing = Spacing.small,
) {
item { ChatListHero(state.hero) }
item {
ConversationFilters(
filters = state.filters,
selected = state.selectedFilter,
onSelect = chatListInteractor::selectFilter,
)
}
conversationSections(
sections = state.sections,
onOpen = { chatListInteractor.open(it) },
)
}
}
The screen still shows the visual skeleton. What changed is that counts, filters, pinned logic, unread logic, sticker hydration, analytics, and route-safe open behavior moved to ChatListInteractor.
Chat session ownership
class ChatSessionInteractor(
private val timeline: MessageTimelineActor,
private val socket: ChatSocketActor,
private val composer: ComposerActor,
private val reactions: ReactionActor,
private val forwarding: ForwardingActor,
) : BasicNode(...) {
val state: StateFlow<ChatScreenState> = combine(...)
fun open(payload: ChatPayload)
fun close()
fun retry(messageId: String)
fun react(messageId: String, reaction: Reaction)
fun showGroupProfile()
}
The parent interactor composes actors. It does not become a second god object. Each actor owns a single workflow, and ChatScreenState is the product-facing shape consumed by Compose.
Profile screen after refactor
@Composable
override fun Content() = themed {
val state by profileInteractor.state.collectAsState()
ScreenColumn(contentPadding = ScreenPadding.feed) {
item { ProfileHero(state.identity, onPhoto = profileInteractor::changePhoto) }
item { ProfilePromptEditor(state.prompts, profileInteractor.prompts) }
item { ProfilePrivacyPanel(state.privacy, profileInteractor.privacy) }
item { ProfileSessionActions(state.session, profileInteractor.sessionActions) }
}
}
This is the target: a reader can see the profile page structure without reading field plumbing, dirty-state logic, geolocation side effects, or save/retry behavior.
File Plan
Most files should stay under roughly 500 lines. The exception is a topology file like BestBuds.kt, where length is acceptable if it lets the app graph be read in one place. The file boundary should follow ownership, not arbitrary line count.
| Area | Proposed files | Notes |
|---|---|---|
| Chat data | ChatListInteractor.kt, ChatSessionInteractor.kt, MessageTimelineActor.kt, ChatSocketActor.kt, ComposerActor.kt, ReactionActor.kt, ForwardingActor.kt | First major split. Keep public action APIs simple. Avoid leaking actor internals to screens. |
| Chat UI | ChatScreen.kt, ChatListScreen.kt, ChatOrganisms.kt, MessageOrganisms.kt, ComposerOrganisms.kt | Keep route-bound skeletons in screen files. Move reusable rows/bubbles/composer pieces into organisms. |
| Profile | ProfileInteractor.kt, ProfileEditorActor.kt, FriendProfileInteractor.kt, GroupProfileInteractor.kt, ProfileOrganisms.kt | Personal, friend, and group profile share identity components but have separate product actions. |
| Onboarding | OnboardingInteractor.kt, OnboardingFlowActor.kt, OnboardingSubmissionActor.kt, OnboardingOrganisms.kt | Move timing, analytics, answers, validation, and completion routing out of UI. |
| Events and campaigns | EventsInteractor.kt, CreateEventInteractor.kt, CampaignInteractor.kt, DiscoverInteractor.kt, EventOrganisms.kt, CampaignOrganisms.kt | Replace sample data and TODO callbacks with graph-owned state and actions. |
| Design system | tokens/, primitives/, atoms/, molecules/, organisms/, theme/ | Use exports that match hierarchy. Keep app-specific product models out of lower layers. |
Migration Phases
The refactor should be done in vertical slices that preserve behavior after each step. The chat domain should move first because it has the most state and the highest market impact.
- List all screen files, design files, interactors, services, repositories, and generated routes.
- Add or refresh smoke tests for login, chat list, chat send, profile, events, and navigation shell.
- Record current Maestro flows that must still pass after each migration slice.
- Mark release-only blockers: dev routes, placeholder callbacks, sample data, empty edit/create routes.
- Create shared patterns for
ScreenState,ActionSet, actor input messages, and reducer-style state updates. - Register interactors as graph nodes and providers where screens can consume them.
- Keep old behavior while routing screen reads through the new state model.
- Extract
ChatListInteractorfromChatsScreenresponsibilities. - Split
ChatInteractorinto session interactor plus socket, timeline, composer, reactions, forwarding, typing/presence actors. - Convert
ChatScreento consumeChatScreenStateand action groups while keeping the visible layout skeleton in the screen file. - Run Android and iOS chat Maestro flows after the slice.
- Move profile fetching, editing, save, logout, privacy, and media workflows into profile interactors.
- Move onboarding progress, responses, validation, analytics, and submission into onboarding actors.
- Make personal, friend, and group profiles share identity atoms and molecules while keeping distinct product organisms.
- Replace sample event data with repository/interactor state.
- Implement create, save, RSVP, open, join, and campaign detail actions as interactor methods.
- Make event cards, campaign cards, recommendation rows, and profile trust panels reusable organisms.
- Rename and move design files into tokens, primitives, atoms, molecules, organisms, and theme scopes.
- Slice broad component contracts into capability facets installed through
themed {}. - Remove empty files, dead imports, compatibility shims, and legacy frontend remnants.
- Run Kotlin/Android, Kotlin/JS, and iOS build checks relevant to the touched layers.
- Run Maestro on Android and iOS for all migrated flows.
- Visually inspect screens for prototype parity and mobile/desktop polish.
- Update docs only after the real app behavior is migrated.
Definition Of Done
The migration is finished only when the real BestBuds apps are migrated, not when the documentation is complete.
Code done
- All user-facing screens use the new design language and atomic components.
- Screens no longer call repositories or services directly except for narrow, documented transitional cases.
- Data plus actions live in interactors. Ordered async workflows live in actors.
- No
BB*compatibility wrappers remain. - No legacy frontend files remain after scanning for valuable behavior.
- Most files stay under 500 lines unless their topology role justifies length.
Validation done
- Android Maestro flows pass with the migrated UI.
- iOS Maestro flows pass with the migrated UI.
- Chat list, chat send, reactions, attachments/stickers, group profile, personal profile, public events, private events, onboarding, and navigation shell are covered.
- Desktop and mobile web prototype parity checks remain useful as design references.
- No critical empty states, TODO callbacks, or release-visible dev surfaces remain.
Guardrails
- Do not add primitives for one use case. A primitive must generalize or stay private.
- Do not use "template" as a way to hide an entire screen elsewhere.
- Do not over-normalize incompatible components into one interface.
- Do not move business logic into design components.
- Do not create graph nodes that only rename an existing service or repository.
- Do not degrade the readability of
BestBuds.kt; it should remain the app topology map. - Do not block real app migration on docs or speculative architecture work.
- Keep the prototype and appWeb as visual references, but app code should have native ownership and state models.
Next Implementation Order
This is the recommended next sequence for actual code work after this document.
- Create a small screen-state/action pattern and register
ChatListInteractor. - Move chat list filtering, pinned/unread sections, sticker hydration, analytics, and open-chat routing into
ChatListInteractor. - Split
ChatInteractorintoChatSessionInteractorplus socket, timeline, composer, reactions, forwarding, and typing/presence actors. - Refactor
ChatScreento renderChatScreenStatewhile preserving the screen's top-level visual skeleton. - Repeat the same pattern for onboarding, profile, events, campaigns, discover, and friends.
- Reorganize the design module into atomic folders and scoped component facets once app usage proves the right boundaries.
- Run Android and iOS Maestro smoke flows after each vertical slice, then the full suite at the end.
Sources
- Local scan of
/Users/ovd/dev/bestbuds/modules/app/src/commonMain/kotlin/ai/bestbuds/app. - Local scan of
/Users/ovd/dev/bestbuds/modules/design/src/commonMain/kotlin/ai/bestbuds/design. - Atomic Design table of contents and Atomic Design Methodology by Brad Frost.