AI for software architecture
Tags: ai, architecture
Published: 2024-02-01 14:40:09

Designing a system, from small to big, starts as a vague process and then incrementally you narrow down
on a particular approach. It'd be absurd to ask someone:
<br/>

> Design a local first webapp with auto and manual saving

or

> Design a system which can onboard users, teams and organisations and manage role based access to resources

<br/>
and expect a fully usable answer right out of the gate in 5 minutes. 
But the urge to do this with AI is quite present, if not immense. 

And to do so, would set you up for failure, more than it'd have by doing the same to you team member. Because
they would understand context, and read between the lines and might actually give something valuable!

And yet, I find myself talking to ChatGPT endlessly about how to build the features I mentioned above.

<br/>
<br/>
# Your experience is context
Before I dive in, let me show you what I mean. The local sync problem I mentioned before, well that was real.


I am quite new to frontend and its mysterious ways of working, but I understand systems. I understand the 
fundamental blocks enough to reason about things, or at least get a sense of direction. 

I am also, a Functional Programming enthusiast, and tried my hand at FRP(Functional Reactive Programming), which led me to RxJS.

Now, the folks who use RxJS as a daily driver must have gotten their wheels turning and good chance they solved it already. 

But not me, I don't use RxJS, I just understand pipelines!

Pipelines can be mapped, filtered, buffered, merged but wait, the events that my app is generating is a stream 
and hence an Observable, even auto save is just a stream of button presses. I think there's something here that can solve my problem.

And that's it!

> A general sense of direction but with concrete ideas is just enough, that current LLMs require to help you with design.


Lemme show you the final code which me and ChatGPT arrived at:
```ts
const manualSaveTrigger$ = typeof window === 'undefined' ? from([]) : fromEvent<KeyboardEvent>(document, 'keydown').pipe(
    filter(event => event.key === 's' && (event.metaKey || event.ctrlKey)),
    map(() => [] as StateChangeEvent<any>[])
);

export function setupSyncPipeline(
    projectId: string,
    eventSource$: NonNullable<SyncManagerState['eventSource$']>,
    syncManagerStore: UseBoundStore<StoreApi<SyncManagerState>>,
    bufferTimeInMs: number,
) {
    const syncManagerState$ = new BehaviorSubject<SyncManagerState>(syncManagerStore.getState());
    syncManagerStore.subscribe(state => syncManagerState$.next(state));

    const sharedEventSource$ = eventSource$.pipe(
        share(),
    );

    const bufferedEvents$ = sharedEventSource$.pipe(
        tap(() => syncManagerStore.getState().setSyncStatus('unsynced')),
        bufferWhen(() => merge(
          timer(bufferTimeInMs), // Emit when buffer time elapses
          manualSaveTrigger$    // Or when manual save is triggered
        )),
        filter(events => events.length > 0)
    );
    
    const result = bufferedEvents$.pipe(
        map(mergeEvents),
        withLatestFrom(syncManagerState$),
        filter(([_, state]) => state.isSyncingAllowed), // Check if syncing is allowed
        tap(() => syncManagerStore.getState().setSyncStatus('syncing')),
        concatMap(([mergedEvents, state]) => 
            ajax<{server_sequence: number}>({
                url: getSyncApiUrl(projectId),
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                },
                body: { updated_artifacts: mergedEvents, server_sequence: state.serverSequence },
                withCredentials: true,
            }).pipe(
                tap({
                    next: (response) => {
                        syncManagerStore.getState().setSyncStatus('synced');
                        syncManagerStore.getState().setServerSequence(response.response.server_sequence);
                    },
                    error: () => {
                        syncManagerStore.getState().setSyncStatus('errored');
                        syncManagerStore.getState().setIsSyncingAllowed(false);
                    }
                })
            )
        )
    );

    const subscription = result.subscribe({
        next: response => console.log('Merged event synced:', response),
        error: err => console.error('Error syncing merged event:', err),
        complete: () => console.log('All merged events synced')
    });

    syncManagerStore.getState().setSubscription(subscription);
}

const mergeEvents = (events: StateChangeEvent<any>[]) => {
    // Merge logic here
    return events.reduce(
        (merged, event) => {
            merged[event.source] = event.newState;
            return merged
        }, 
        {} as Record<string, any>
    );
};

function getSyncApiUrl(id: string) {
    return `${getBaseAPIPath()}/project/sync/${id}`;
}
```

That's a lot of code, but that's not important. What's important is it works, I understand how it works, and I got this by understanding basics.

<br/>
<br/>
<br/>
<br/>

# It's the new engineer in your team
The one who is quite eager to execute but needs some hand holding, and with good direction
can deliver exactly what you want. That's what current LLMs are when it comes to architecture.

You have to be the one boucing ideas, guiding and more importantly verifying, because they'll definitely make mistakes. Here are the ones it made in my case:
<br/>

When it missed an operation:
>Indeed, you are correct. Let's integrate the merging of events into the updated pipeline, ensuring that the merged events and the sequence number are correctly handled in each API call.

<br/>

After reviewing that it missed a crucial check:
>Yes, you are correct. The current implementation does not check if syncing is allowed before proceeding with the API call. This check is crucial, especially in the scenario where the state is set to 'errored' and further syncing is supposed to be halted.

<br/>

When it just assumed how TS works:
>You're correct to question the typecasting of useStore to Observable\<StoreState> in the RxJS pipeline.

<br/>

When it came up with the wrong implementation:
>You're correct in your understanding. Placing the tap operator after bufferTime means it will only execute after the specified time interval (n seconds) has passed and the events have been buffered.

<br/>

Once again, wrong implementation:
>Yes, you're correct. Setting the 'unsynced' status after bufferTime would introduce the same issue as discussed earlier. The 'unsynced' status would only be set after the buffering period has elapsed, not immediately when events start arriving.

<br/>

When it fumbled manual save:
>You're absolutely correct. If the manualSave$ observable emits an empty array, and it's merged with the event source observable, there's a risk of syncing that empty array instead of the actual buffered events.

<br/>
But what it lacks in precision, it makes up for in speed. The chat interaction might look long but the conversation lasted
for barely an hour and by the end of it, I had exactly what I needed.

Here are some more design problems I could rush through quickly:

- That user, projects, organisation and resource problem I mentioned before, very real.
- I also needed to create a basic file system abstraction, I know how trees work but need help with applying that knowledge
to the app I am building.
- In the [metz](https://metz.sh/) runtime I needed a better way to detect connections, I had an idea around a graph based approach
but needed some oversight.

<br/>
<br/>
# Compiler of Ideas
A programming language compiler, takes our intention of what we want to do, and then does all the hard work of
making it a reality.

And that's how I see my interactions with AI for design. It too has "syntax" errors, where if you don't start
with something concrete it'll "crash" and give garbage answers.

Designing a system needs in-depth analysis, but not just of one idea, rather of many ideas. 

>System design is not just about choosing the right thing, but knowing there are other right things and this thing is the most right for us.

This is where iteration speed matters. The ability to try things out quickly, create a thesis and compare, is crucial to arriving at an informed decision.

And this is exactly were AI shines.


_PS: This post appeared first at [metz's blog](https://lab.metz.sh/ai-for-architecture).
We at [metz](https://metz.sh/) are building a platform where you can just code to create architecture diagrams._