Reaktor DocsBestBudsArchitecture Refactor

BestBuds - App Architecture

BestBuds Architecture Refactor Plan

A concrete plan for applying atomic design and the React/actor philosophy across the real BestBuds app: fewer primitive concepts, stronger composition, clearer graph ownership, and a hard separation between data, actions, state, and UI.

Use whenRefactoring modules/app and modules/design after the prototype-style UI migration.
SourceCodex scan of BestBuds screens, interactors, services, graph wiring, and design components.
ModelReaktor graph + nested interactors + focused actors + atomic Compose components.
Route/docs/bestbuds-architecture-refactor-plan

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.

Keep

The Reaktor graph, port wiring, route bindings, edge.push(payload) ergonomics, scoped themed {} UI, new design tokens, prototype visual direction, and existing data/service work.

Change

Move repository access, analytics, network refresh, optimistic messaging, editor state, and transient UI flows out of screens and into graph-owned interactors or actors.

Reject

Do not create broad one-use templates, BBCard-style wrappers, random service primitives, labels that only satisfy one client, or files that hide the visual structure of a screen.

Architecture standard: opening a screen file should let a reader visualize the layout and the main product concepts. Opening an interactor should show data, actions, and side effects. Opening an actor should show one coherent loop.

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.

HotspotObserved shapeRefactor pressure
ChatInteractor.ktRoughly 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.ktCollects 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.ktClose 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.ktMixes profile fetch, editor fields, dirty detection, session state, geo side effects, analytics, logout, and UI model shaping.Create ProfileInteractor and ProfileEditorActor.
OnboardingScreen.ktOwns questions, step state, responses, timings, analytics, submission, completion, and route decisions.Create OnboardingInteractor with flow actor and submission actor.
PublicEventsScreen.ktUses 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/designStrong visual foundation, but component contracts are broad and some files are large or implementation-oriented.Introduce atomic folders and narrower capability scopes under themed {}.
Important: file splitting is not the goal by itself. Splitting only helps when every new file has a stable abstraction level and a reason to be read independently.

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.

LayerOwnsMust not ownExample
ServiceTyped transport contracts and request/response shapes.Screen state, app labels, UI concepts, cache policy, or operation names for HTTP handlers.MessagingApi, SocialApi, StickerApi.
RepositoryPersistence, cache, local source of truth, and remote fetch wrappers.Screen composition, analytics, navigation, or transient UI workflow.UserRepository, MessageRepository, SocialRepository.
InteractorData plus actions. Combines repositories, services, analytics, and route-safe domain operations.Low-level Compose layout or primitive style choices.ChatListInteractor, ProfileInteractor, EventsInteractor.
ActorOne focused async loop, reducer, queue, or state machine.Multiple unrelated workflows or broad product orchestration.ChatSocketActor, ComposerActor, OnboardingSubmissionActor.
Screen stateImmutable state model shaped for UI rendering.Networking, mutation logic, or Compose dependencies.ChatScreenState, ProfileEditorState.
ScreenRoute 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 componentReusable rendering and interaction primitives at the right atomic level.Product data fetching or route behavior.Card, ConversationRow, MessageTimeline, ProfileHeader.
Gold standard: 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 stageBestBuds meaningExamplesRules
TokensSubatomic 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 / atomsSmallest functional UI elements.Text, IconAction, Card, Button, Avatar, TextField, Badge.No repository, no product workflow, minimal styling branches, stable accessibility contract.
MoleculesSmall reusable groups with one purpose.SearchBar, PillGroup, StatRow, ProfileFact, ComposerActionStrip.Use atoms and tokens. Do not become a full screen section.
OrganismsDistinct product sections composed from molecules and atoms.ConversationRow, MessageTimeline, ProfileHeader, EventCard, CampaignStageTrack.Can know product UI models, but not repositories or network behavior.
Screen blocksNamed 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 / pagesRoute-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.
BestBuds adaptation: use atomic design as a hierarchy, not dogma. If a screen-specific block is not reused but makes the screen readable, it can stay private in that screen file. If moving it elsewhere makes the screen opaque, do not move it.

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 objectRecommended roleBestBuds application
BestBudsReadable 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.
ServiceNodeTyped API provider for real services.Expose MessagingApi, SocialApi, and StickerApi. No ad hoc client service labels.
RepositoryNodeOffline-first data access and cache lifecycle.User, message, sticker, social, and profile data should live here.
InteractorProduct behavior boundary.Chat, events, campaigns, friends, onboarding, profile, palette/session.
ActorNodeFocused event loop where ordering matters.Socket, composer, forwarding, onboarding submission, media upload, profile save.
RouteBindingRoute payload and navigation actions.Use edge.push(payload), edge.replace(payload), and correct return/back commands.
Rule: if a screen has to coordinate two repositories or a repository plus a service, it probably needs an interactor. If it coordinates concurrency, retries, or ordered events, it probably needs an actor.

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 groupCurrent concernTarget screen shapeInteractor target
Start / authLogin 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.
OnboardingQuestions, 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 listFiltering, 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 threadLarge 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 profileBeautiful 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 profileFetch, 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 profileDirect repository data and simple profile display.Screen declares friend hero, prompts, mutual circles, shared events, plan/message actions.FriendProfileInteractor.
Campaigns / discoverStrong cards, shallow actions, no detail depth.Screen declares stage summary, recommendation explanation, campaign sections, detail entry.CampaignInteractor and DiscoverInteractor.
Public/private eventsSample data and local save state; create/open/join not real enough.Screen declares event hero, filters, event feed, create FAB, detail sheets/routes.EventsInteractor.
FriendsSimple list with profile routing.Screen declares search, close friends, recent activity, groups, plans, empty recovery.FriendsInteractor.
Palette / devUseful 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, expose ChatScreenState, 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.
Interactor API shape: prefer 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.

PackageContentAllowed dependenciesForbidden dependencies
design.tokensColor, typography, spacing, corners, motion, elevations.Kotlin/Compose primitives.App models, screens, repositories.
design.primitivesFoundational UI elements: surface, text, icon action, image, input, button.Tokens and Compose primitives.Product models, network state.
design.atomsBadges, pills, stats, labels, dividers, avatars, menu items.Primitives and tokens.Screen-specific branching.
design.moleculesSearch bars, action rows, input groups, summary rows, media pickers.Atoms and primitives.Repository calls, navigation.
design.organismsConversation 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.

Extract when

  • The name is a real product or UI concept: ConversationRow, ProfileHeader, EventCard, ComposerDock.
  • The component is reused or likely to become a stable visual contract.
  • The extracted code sits one abstraction level below its caller.
  • The component bundles accessibility, interaction, and visual behavior that should stay consistent.
  • The screen becomes easier to visualize after extraction.

Do not extract when

  • The new component is a one-use template that makes the screen say only PrivateEventsTemplate(...).
  • The extracted function name merely repeats the file name.
  • The component takes a giant prop list because it is hiding unrelated state.
  • The abstraction exists only to avoid a few lines of readable layout code.
  • The extraction forces unrelated concepts into a common interface.
Screen readability rule: a screen should reveal its regions directly. A good screen says: hero, filters, sections, feed, sheet, dock, FAB. It should not say: template.

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.

AreaProposed filesNotes
Chat dataChatListInteractor.kt, ChatSessionInteractor.kt, MessageTimelineActor.kt, ChatSocketActor.kt, ComposerActor.kt, ReactionActor.kt, ForwardingActor.ktFirst major split. Keep public action APIs simple. Avoid leaking actor internals to screens.
Chat UIChatScreen.kt, ChatListScreen.kt, ChatOrganisms.kt, MessageOrganisms.kt, ComposerOrganisms.ktKeep route-bound skeletons in screen files. Move reusable rows/bubbles/composer pieces into organisms.
ProfileProfileInteractor.kt, ProfileEditorActor.kt, FriendProfileInteractor.kt, GroupProfileInteractor.kt, ProfileOrganisms.ktPersonal, friend, and group profile share identity components but have separate product actions.
OnboardingOnboardingInteractor.kt, OnboardingFlowActor.kt, OnboardingSubmissionActor.kt, OnboardingOrganisms.ktMove timing, analytics, answers, validation, and completion routing out of UI.
Events and campaignsEventsInteractor.kt, CreateEventInteractor.kt, CampaignInteractor.kt, DiscoverInteractor.kt, EventOrganisms.kt, CampaignOrganisms.ktReplace sample data and TODO callbacks with graph-owned state and actions.
Design systemtokens/, 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.

Phase 0 - Safety net and inventory1 pass
  • 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.
Phase 1 - Interactor scaffoldingFoundation
  • 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.
Phase 2 - Chat splitHighest leverage
  • Extract ChatListInteractor from ChatsScreen responsibilities.
  • Split ChatInteractor into session interactor plus socket, timeline, composer, reactions, forwarding, typing/presence actors.
  • Convert ChatScreen to consume ChatScreenState and action groups while keeping the visible layout skeleton in the screen file.
  • Run Android and iOS chat Maestro flows after the slice.
Phase 3 - Identity and onboardingTrust foundation
  • 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.
Phase 4 - Events, campaigns, and discoveryIRL loop
  • 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.
Phase 5 - Design taxonomy cleanupAtomic pass
  • 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.
Phase 6 - ValidationDone bar
  • 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.
Hard bar: a visual migration without Maestro validation is not done. A clean component taxonomy without migrated app screens is not done. A screen that looks right but still owns business logic is not done.

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.

  1. Create a small screen-state/action pattern and register ChatListInteractor.
  2. Move chat list filtering, pinned/unread sections, sticker hydration, analytics, and open-chat routing into ChatListInteractor.
  3. Split ChatInteractor into ChatSessionInteractor plus socket, timeline, composer, reactions, forwarding, and typing/presence actors.
  4. Refactor ChatScreen to render ChatScreenState while preserving the screen's top-level visual skeleton.
  5. Repeat the same pattern for onboarding, profile, events, campaigns, discover, and friends.
  6. Reorganize the design module into atomic folders and scoped component facets once app usage proves the right boundaries.
  7. Run Android and iOS Maestro smoke flows after each vertical slice, then the full suite at the end.
Best first cut: start with chat because it will prove the architecture under realtime, optimistic, paginated, media-rich, sheet-heavy conditions. If the pattern works there, it will work everywhere else.

Sources