Complete Architecture Guide
Last Updated: December 15, 2024
Target Audience: Developers, architects, new team members
Scope: Modern C++26 architecture, reactive patterns, state management, threading model
This is the comprehensive architecture guide for the Sidechain VST Plugin. It covers everything from high-level principles to practical implementation patterns.
Quick Start (5 Minutes)
Your First Store Integration
Want to understand the modern architecture immediately? Here’s the complete flow:
Step 1: Subscribe to a Store
class MyFeedComponent : public ReactiveBoundComponent {
private:
std::shared_ptr<PostsStore> postsStore;
std::function<void()> unsubscribe;
public:
void setPostsStore(std::shared_ptr<PostsStore> store) {
postsStore = store;
// Subscribe: This callback runs whenever state changes
unsubscribe = postsStore->subscribe([this](const PostsState& state) {
displayPosts(state.getCurrentFeed().posts);
if (state.getCurrentFeed().isLoading) {
showLoadingSpinner();
}
repaint();
});
}
~MyFeedComponent() override {
if (unsubscribe) unsubscribe(); // Always cleanup!
}
};
Step 2: Trigger Actions via Store Methods
void MyFeedComponent::onLikeButtonClicked(const FeedPost& post) {
// Call store method - it handles everything:
// 1. Optimistic update (UI changes immediately)
// 2. Server sync (network call happens async)
// 3. Error handling (automatic rollback if needed)
postsStore->toggleLike(post.id);
// The store will notify all subscribers, UI updates automatically
}
Step 3: Store Updates Happen Automatically
// Inside PostsStore (implementation detail you don't need to write)
void PostsStore::toggleLike(const juce::String& postId) {
// 1. Optimistic update
auto state = getState();
if (auto* post = findPost(postId, state)) {
post->isLiked = !post->isLiked;
post->likeCount += post->isLiked ? 1 : -1;
}
setState(state); // Notifies all subscribers (including MyFeedComponent!)
// 2. Server sync (async, doesn't block UI)
networkClient->likePost(postId, [this, postId](auto result) {
if (!result.isOk()) {
toggleLike(postId); // Rollback if failed
}
});
}
That’s it! Your UI automatically updates because:
Store state changed →
setState()calledAll subscribers notified → subscription callback fires
Your component re-paints with new data
No manual state management. No callback chains. Just reactive magic. ✨
Core Principles
The Sidechain plugin architecture is built on six core principles:
- Single Source of Truth
One store per entity type (PostsStore, UserStore, ChatStore, etc.)
Not separate stores for different views of the same data
Prevents state desynchronization
- Reactive Updates
Components subscribe to stores
Automatically re-render on state changes
No manual UI updates needed
- Immutable State
State objects are copied and replaced, never mutated
Ensures predictability and thread safety
- Optimistic Updates
UI updates immediately for instant feedback
Server sync happens asynchronously in background
Automatic rollback if server rejects
- Error Handling
Errors stored in state, not thrown as exceptions
UI displays error messages from state
Automatic retry logic built into stores
- Type Safety
Full C++26 typing throughout
Smart pointers (no raw
new/delete)Strong-typed state structs
Architecture Layers
The application follows a layered architecture:
┌─────────────────────────────────────────┐
│ UI Components │
│ (PostsFeed, Profile, etc.) │
│ (ReactiveBoundComponent subclasses) │
└────────────────┬────────────────────────┘
│ subscribe to
▼
┌─────────────────────────────────────────┐
│ Stores (State Management) │
│ (PostsStore, UserStore, ChatStore) │
│ (Store<T> + Reactive Bindings) │
└────────────────┬────────────────────────┘
│ read/write via
▼
┌─────────────────────────────────────────┐
│ NetworkClient │
│ (HTTP + WebSocket) │
│ (Rate limiting, error tracking) │
└────────────────┬────────────────────────┘
│ sends/receives
▼
┌─────────────────────────────────────────┐
│ Backend API │
│ (Go server + getstream.io) │
│ (REST endpoints + WebSocket) │
└─────────────────────────────────────────┘
The 9 Core Stores
The application uses 9 centralized stores for entity management:
Store |
Purpose |
Key State |
|---|---|---|
PostsStore |
All posts: feeds, saved, archived |
|
UserStore |
All users: current, cached, discovery |
|
ChatStore |
Messages and channels |
|
DraftStore |
Draft posts (singleton) |
|
CommentStore |
Comments on posts |
|
NotificationStore |
Notifications and unread counts |
|
StoriesStore |
Stories and highlights |
|
UploadStore |
Upload progress and state |
|
FollowersStore |
Follower/following lists |
|
See Store Pattern for detailed documentation on the store pattern and how each store works.
Observable Collections
The architecture uses reactive collections for fine-grained updates:
ObservableProperty<T>: Reactive wrapper for single values
auto property = std::make_shared<ObservableProperty<int>>(0);
// Subscribe to changes
auto unsubscribe = property->subscribe([](int newValue) {
std::cout << "Value changed to: " << newValue << std::endl;
});
// Setting value notifies subscribers
property->setValue(42);
ObservableArray<T>: Reactive array with batch notifications
auto posts = std::make_shared<ObservableArray<FeedPost>>();
// Subscribe to array changes
posts->subscribe([](const ArrayChangeEvent<FeedPost>& event) {
switch (event.type) {
case ChangeType::ItemAdded:
std::cout << "Post added at index " << event.index << std::endl;
break;
case ChangeType::BatchUpdate:
std::cout << "Batch update with " << event.items.size() << " items" << std::endl;
break;
}
});
ObservableMap<K, V>: Reactive key-value store
auto userCache = std::make_shared<ObservableMap<juce::String, User>>();
userCache->subscribe([](const MapChangeEvent<juce::String, User>& event) {
if (event.type == ChangeType::ItemAdded) {
std::cout << "User " << event.key << " cached" << std::endl;
}
});
See Observable Pattern for detailed documentation on reactive collections.
Data Flow Patterns
Feed Loading Flow
The feed loading follows this flow:
UI calls loadFeed()
↓
PostsStore marks isLoading=true
Notifies subscribers
↓
UI shows loading spinner
[Meanwhile, async...]
NetworkClient fetches from server
↓
Server responds with posts
↓
PostsStore updates posts array
Sets isLoading=false
Notifies subscribers
↓
UI renders posts
Optimistic Update Flow
User interactions use optimistic updates:
User clicks "Like"
↓
Store updates immediately:
- isLiked = true
- likeCount++
Notifies subscribers
↓
UI updates instantly
[Meanwhile, async...]
NetworkClient sends to server
↓
If server fails:
Rollback by toggling again
If server succeeds:
State persists
See Data Flow Patterns for detailed data flow diagrams and patterns.
Threading Model
The plugin uses a multi-threaded architecture with strict constraints:
Audio Thread (Real-Time, Lock-Free)
✅ Allowed:
Read from
atomic<>variablesPre-allocated buffers
Ring buffers for lock-free IPC
❌ Forbidden:
Any locks (mutex, semaphore)
Memory allocation (
new,malloc)File I/O
Network calls
Anything that could block
Message Thread (UI Thread, JUCE)
✅ Allowed:
Update components
Call
repaint()Synchronous operations
❌ Forbidden:
Network calls (use
Async)File I/O (use
Async)Long computations
Background Threads (Network, File I/O)
✅ Allowed:
Network requests
File I/O
Long computations
❌ Forbidden:
Direct UI updates (use
callAsync)Audio buffer manipulation
Async Pattern
All network operations use the Async helper:
// Background work → message thread callback
Async::run<juce::Image>(
// Background work
[this]() {
return downloadImage(url); // On background thread
},
// UI callback
[this](const auto& image) {
setImage(image); // On message thread
repaint();
}
);
See Threading Model for detailed threading constraints and patterns.
Component Patterns
Container/Presentation Pattern
Components use a two-tier pattern:
Container Component (Smart, manages state):
class PostsFeed : public ReactiveBoundComponent {
private:
std::shared_ptr<PostsStore> postsStore;
std::function<void()> unsubscribe;
public:
void setPostsStore(std::shared_ptr<PostsStore> store) {
postsStore = store;
unsubscribe = postsStore->subscribe([this](const PostsState& state) {
updatePostsList(state.getCurrentFeed().posts);
repaint();
});
}
void onPostCardLikeClicked(const FeedPost& post) {
postsStore->toggleLike(post.id); // Store handles everything
}
~PostsFeed() override {
if (unsubscribe) unsubscribe();
}
};
Presentation Component (Dumb, receives props):
class PostCard : public Component {
private:
FeedPost post; // Received as prop, not managed
public:
void setPost(const FeedPost& newPost) {
post = newPost;
repaint();
}
// Callbacks bubble up to parent
std::function<void(const FeedPost&)> onLikeClicked;
std::function<void(const FeedPost&)> onCommentClicked;
};
See Reactive Components for detailed component patterns.
Testing Strategies
Unit Testing Stores
class PostsStoreTest : public juce::UnitTest {
public:
void runTest() override {
// Setup: Create store with mock client
auto mockClient = std::make_shared<MockNetworkClient>();
auto store = std::make_shared<PostsStore>(mockClient.get());
// Record state changes
juce::Array<PostsState> stateHistory;
store->subscribe([&](const PostsState& state) {
stateHistory.add(state);
});
// Act: Trigger action
store->toggleLike("post-123");
// Assert: Check state changed correctly
expect(stateHistory.size() > 0);
expect(stateHistory.back().getCurrentFeed().posts[0].isLiked == true);
}
};
Common Best Practices
Immutable Updates
✅ Good: Create new state, replace entirely
{
auto state = getState(); // Copy
state.posts.add(newPost); // Modify copy
setState(state); // Replace entire state
}
❌ Bad: Mutate state directly
{
auto& state = getState(); // Direct reference (wrong!)
state.posts.add(newPost); // Mutating original (wrong!)
}
Error Handling
Store errors in state, don’t throw:
{
auto state = getState();
state.error = "Failed to load feed";
state.isLoading = false;
setState(state);
}
UI subscribes and displays error:
store->subscribe([this](const PostsState& state) {
if (!state.error.isEmpty()) {
showErrorNotification(state.error);
}
});
Subscription Cleanup
Always cleanup in destructor:
class MyComponent : public Component {
private:
std::function<void()> unsubscribe;
public:
void bindToStore(std::shared_ptr<PostsStore> store) {
unsubscribe = store->subscribe([this](const PostsState& state) {
// Handle state changes
});
}
~MyComponent() override {
if (unsubscribe) {
unsubscribe();
}
}
};
Troubleshooting
Component Not Updating When Store Changes
Problem: Subscribe to store but UI doesn’t update
Causes:
Didn’t save unsubscribe function
Component was deleted before state changed
Not calling
repaint()in subscription
Fix:
auto unsubscribe = store->subscribe([this](const PostsState& state) {
updateUI(state);
repaint(); // Important!
});
// ... later ...
if (unsubscribe) unsubscribe(); // Cleanup!
State Changes But Nobody Notified
Problem: Called setState() but subscribers didn’t get called
Causes:
Mutated state instead of creating new one
Comparing same object (not deep copy)
Fix:
// Create copy before modifying
auto state = getState(); // Makes copy
state.posts.add(newPost); // Modify copy
setState(state); // Notify subscribers
Memory Leaks from Store Subscriptions
Problem: Component destroyed but never unsubscribed
Fix:
class MyComponent {
private:
std::function<void()> unsubscribe;
public:
~MyComponent() override {
// Call in destructor!
if (unsubscribe) unsubscribe();
}
};
See Also
Store Pattern - Store pattern and all 9 stores
Observable Pattern - Observable collections reference
Reactive Components - Component integration patterns
Data Flow Patterns - Data flow diagrams and patterns
Threading Model - Threading model and constraints
Service Layer - Business logic layer
/MODERNIZATION_EVALUATION_REPORT.md - Complete project assessment
/CLAUDE.md - Development commands and guidelines