Engineering Stability for Vibe Coding: Specs, Architecture, and Changelogs
Turn an AI-assisted change into an admitted, implementable, verifiable, traceable, and reversible engineering milestone. Domain boundaries and testable frontend state are key parts of the larger delivery system.
The project I have been building is no longer a small demo with a few pages. It combines market research, immutable strategy versions, historical backtests, parameter experiments, paper and live trading, notifications, a global Agent, realtime market data, asynchronous workers, and an independent ingestion backend.
When Vibe Coding accelerates a system like this, the first limitation is rarely that the model cannot write code. The real problem is that the system does not give generated code enough boundaries to know what it must not do.
Ask an AI to add a Save button and it may put fetching, validation, retries, optimistic state, and error recovery in the component. Ask it to add a background job and it may keep the status in memory before telling the browser that the work finished. A later coding session then creates another rule that looks similar but behaves differently.
Every local change can appear to work while the combined product becomes harder to prove.
I eventually found that one architecture refactor was not enough. What the project needed was an engineering change system: specifications decide whether a request is admitted, historical-conflict checks prevent AI from rewriting retained decisions, domain boundaries constrain implementation, layered tests provide evidence, and the changelog reconnects requirements, code, verification, and rollback.
Domain-driven design and frontend-state separation remain important, but they are one part of the system. The main question is broader: how can an engineering process make a Vibe Coding change admissible, implementable, verifiable, traceable, and reversible?
The short answer: stability comes from a complete change chain
I now treat a change as this state machine:
user request
-> specification admission
-> current and historical conflict check
-> owning specification changes first
-> domain and state-authority design
-> implementation
-> layered verification
-> changelog records actual evidence
-> version and indexes stay synchronized
-> reversible handoff
Missing any stage creates a familiar kind of “locally complete, globally false” result:
- Without specification admission, generated code makes product decisions.
- Without a history check, a new session overwrites an earlier compatibility decision.
- Without domain and state authority, rules spread across Views, Actions, Workers, and Socket handlers.
- Without layered verification, visible copy is mistaken for correct failure, concurrency, and recovery behavior.
- Without changelog evidence, the next tester does not know what must be proved.
- Without version, index, and rollback discipline, the change cannot be located, discovered, or safely reversed.
Stability does not mean the model writes everything correctly on the first attempt. It means the engineering system rejects mistakes early and at the cheapest capable boundary.
The first gate is specification admission
In this project, features, bug fixes, refactors, compatibility work, migrations, APIs, tests, and interaction changes first locate the permanent specification that owns the behavior.
Admission checks five things:
- What observable user outcome is required?
- Which domain object and use case own it?
- What does the current specification already promise?
- Does the retained changelog contain an opposite or stricter decision?
- Which behavior, invariants, failure semantics, and acceptance points must be explicit before implementation?
When there is no conflict, the specification diff enters the working change before runtime code. A bug fix first clarifies the violated invariant and regression point. A behavior-preserving refactor first records preserved external behavior, permitted internal boundary changes, and verification scope.
When current specifications disagree, or a proposal conflicts with retained history, the model cannot choose by recency, specificity, or compatibility with existing code. It must stop and present the proposed value, conflicting clauses and evidence, current implementation, minimum compatibility options and debt, separate migration/refactor options and rollback, and the exact human decision required.
This looks like a documentation gate, but it is really an authorization boundary. It prevents a coding model from acquiring product-decision authority. A specification is not a report written after implementation; it is the admission ticket required before implementation starts.
Start with the product loop, not the framework
The product is called StratThread. Its job is not merely to display market data. It supports a traceable research loop:
discover a market
-> form a research judgment
-> create or choose an owned strategy
-> freeze an immutable strategy version
-> backtest it and inspect holdout evidence
-> compare bounded parameter experiments
-> observe it through paper trading
-> enter controlled live execution or notification-only mode
-> create a new version from retained evidence
Once this loop is explicit, the important domain objects become visible: users, market instruments, strategies, strategy versions, backtests, experiments, paper runs, live accounts, automations, push destinations, and notifications.
Domain design does more than name them. It establishes facts that implementation cannot casually rewrite:
- Identity comes from server authentication; a View, Socket packet, or Agent argument cannot select an owner.
- A strategy version that has been executed is immutable. Source changes create a new version.
- A run freezes its exact version, parameters, market, and assumptions.
- Completed backtest and execution evidence cannot be recalculated over newer data and silently replaced.
- High-risk operations have explicit capability, approval, idempotency, and audit boundaries.
- Research scores and simulations are evidence, not promises of future return.
These decisions exist before React, Go, or PostgreSQL. Technology carries the model; it does not define the product by accident.
The full system is a set of responsibilities
The runtime topology can be reduced to this map:
Browser / optional native shell
-> Astro SSR + authenticated Actions
-> React interactive islands
-> Application Services
-> Repository / Engine ports
-> PostgreSQL / Redis / ClickHouse
Realtime gateway
-> authenticated subscriptions
-> replaceable quote, progress, and invalidation hints
Worker roles
-> backtest / Agent / paper trading / live trading
-> recover exact work from durable records
Independent Go backend
-> market catalogue and ingestion
-> confirmed history to ClickHouse
-> provisional current bars to expiring Redis state
-> operator control plane
Four application boundaries carry distinct responsibilities:
| Boundary | Owns | Does not own |
|---|---|---|
| Web | SSR, authenticated interaction, Actions, React pages | durable truth in browser state |
| Realtime | market observation, progress, invalidation, delivery hints | final business results |
| Worker | asynchronous Backtest, Agent, Paper, and Live execution | recovery from process memory |
| Backend | market catalogue, ingestion, and operations | duplicated user Strategy workflows |
Every process is replaceable. Losing an instance must not lose a strategy, task, account, or result. Process-local state is limited to clients, pools, compiled schemas, loggers, and one operation’s bounded working set.
That statelessness rule is especially useful with AI-generated code. A model often reaches for a module-level map, singleton Store, or writable file to finish the current function. Telling it that any instance may disappear rejects a large class of incorrect shortcuts before they spread.
Application Services keep multiple entry points consistent
One use case may be entered through a browser Action, an Agent Tool, a Socket handler, a Worker recovery path, or an operator command. Duplicating authorization, validation, and transitions in every adapter creates semantic drift after only a few coding sessions.
The project therefore centers business workflows in Application Services:
Action -----------+
Agent Tool -------+--> ActorContext -> Application Service -> Repository / Engine port
Socket handler ---+
Worker -----------+
Each entry point adapts input: it constructs trusted server context, validates its transport envelope, calls one use case, and maps the result. Ownership, state transitions, persistence orchestration, and domain errors remain in one service boundary.
This also makes the Agent a controlled adapter rather than another business system. A model may propose a strategy mutation or a Backtest, but prompt text cannot establish identity and Tool code cannot bypass the same Service used by Web. Writes still pass through the relevant approval and audit boundary.
Frontend business APIs also share one registered Action/Tool contract and one input schema. The browser and Agent see the same product capability through different presentations.
Assign one authority to every kind of state
Many state bugs are not caused by having too much state. They happen because several copies all believe they are authoritative.
This system uses a simple authority table:
| State | Authority | Replaceable projection |
|---|---|---|
| users, strategies, versions, runs, accounts, intents, audit, notifications | PostgreSQL | browser Stores, realtime hints |
| confirmed historical candles | ClickHouse | chart data |
| sessions, queues, leases, expiring progress, current open candles | namespaced Redis with TTLs | process handles, Socket rooms |
| drafts, filters, dialogs, chart viewport | browser memory | safe to lose on refresh |
The table directly changes implementation. A backtest.completed Socket message is not the result; it is a reason to perform an owner-scoped authoritative read. A missed packet may delay presentation, but it cannot delete the business fact.
Likewise, a Redis outage cannot silently fall back to a local map and return a plausible empty result. Dependency failures remain visible, and readiness distinguishes a live process from a runtime that can actually serve its required data path.
Reliable async work is about ordering, not merely a queue
Backtests, experiments, Agent runs, and continuous trading cannot safely live inside one HTTP request. Browsers close, Workers restart, and queues redeliver.
A reliable path looks like this:
validate
-> commit queued record and delivery outbox in PostgreSQL
-> dispatcher delivers a compact record reference
-> Worker reloads frozen input
-> commit running
-> execute one isolated domain engine
-> commit completed / failed and immutable evidence
-> publish realtime or Push hints last
The critical rule is persist before delivery. When the business record and outbox commit together, a delayed dispatcher can always continue the work. Delivering to the queue first risks a message whose authoritative input never existed.
Queue delivery is treated as at least once, so the Run ID is idempotent. A repeated message resumes or reconciles the same Run instead of creating a second result. Redis leases prevent concurrent work temporarily, but leases are not business authority; a replacement Worker recovers from PostgreSQL checkpoints.
For high-risk external effects, the system persists the intent and stable idempotency identity before calling the provider. An unknown transport outcome is not automatically retried unless the external contract makes that retry provably safe.
Realtime owns speed; storage owns truth
Realtime transports current observation, progress, and invalidation. They make the UI responsive without becoming the only copy of a strategy, result, order, or notification.
Market data follows the same rule. The current incomplete candle may exist in an expiring Redis projection; a confirmed candle enters ClickHouse. Interactive charts may combine both. Historical Backtests use confirmed data only, so a moving candle cannot enter immutable evidence.
This distinction matters in AI-assisted code because appending “the latest value” to a history array is an easy local implementation. The domain contract must explain that observation and confirmed fact are different types of data.
Frontend focus: a View should not know how Save completes
The most important stability problem remains frontend logic.
A conventional component often loads in useEffect, saves in a click closure, and distributes loading, errors, drafts, and server state across multiple useState calls. Tests must render the complete page, click through it, and wait for copy. A visual redesign then breaks business tests.
The current frontend follows one dependency direction:
View -> Page -> page-scoped Provider -> Domain Store -> Repository -> SDK / Action
- The View selects state, renders it, and emits intent.
- The Page composes the boundary and calls
store.init()once. - The Provider creates one Store for the mounted scope, injects the Repository, and calls
destroy()on teardown. - The Domain Store owns the state machine, validation, async workflow, concurrency, optimistic reconciliation, errors, and cleanup.
- The Repository exposes domain-friendly capabilities and maps SDK/Action transport.
A Save button becomes deliberately uninteresting:
const save = useEditorStore((state) => state.save)
const saving = useEditorStore((state) => state.status === 'saving')
return <button onClick={() => void save()} disabled={saving}>Save</button>
It does not know the endpoint, revision rule, confirmed snapshot, rollback policy, or whether an older request is stale.
A Store is an executable state machine, not a data bucket
Moving a pile of useState fields into a global object does not solve the problem. The Store should express meaningful states and events:
idle -> loading -> ready | empty | error
ready -> editing -> validating -> saving -> ready | conflict | error
ready -> destroyed
Lifecycle and concurrency are explicit. Store implementations keep request generations, lifecycle generations, and AbortControllers. A result may update state only if the request and mounted lifecycle are still current.
const requestVersion = ++snapshotRequestVersion
const requestLifecycle = lifecycleVersion
const snapshot = await repository.getSnapshot(id, controller.signal)
if (
requestVersion !== snapshotRequestVersion ||
requestLifecycle !== lifecycleVersion
) return
set((state) => {
state.snapshot = snapshot
})
That makes concrete behavior provable:
- Strict Mode duplicate initialization does not duplicate meaning.
- A slow response for an older selection cannot overwrite the newest selection.
- A late request cannot revive a destroyed Store.
- A failed optimistic save restores only the affected confirmed state and preserves later user input.
These properties are hard to test inside component effects and closures. In a framework-neutral Store, they are events and state assertions.
Frontend behavior can be tested without rendering the page
The Store receives a Repository port, so tests inject a fake and call actions directly:
const repository = {
getSnapshot: jest.fn(),
saveSnapshot: jest.fn(),
}
const store = createEditorStore(repository)
const first = store.getState().selectVersion('v1')
const second = store.getState().selectVersion('v2')
await Promise.all([first, second])
expect(store.getState().selectedVersion).toBe('v2')
The suite covers initialization success and failure, retries, concurrent calls, empty and boundary data, every transition, optimistic confirmation and rollback, revision conflict, out-of-order responses, later user input, destroy-time aborts, and resource cleanup. Repository tests separately prove request construction, mapping, and error translation.
View tests can then focus on semantics, discoverable actions, keyboard paths, focus, accessible names, and honest status feedback. “Is the business behavior correct?” and “Is it presented correctly?” become two independently diagnosable failures.
A changelog is an executable acceptance contract
A specification defines what should be true. The changelog records what this milestone changed and which evidence proves it.
The root version advances once for every independently reviewable and verifiable milestone. Every version has a matching changelog entry that preserves this mapping:
Requirement -> Implementation -> Verification
A useful entry includes:
- every requirement and its owning specification;
- concrete application, domain, protocol, storage, or interaction boundaries changed;
- reproducible commands or tools, preconditions, expected results, and actual results;
- compatibility, migration impact, known limitations, and residual risk;
- rollback steps and durable business data that must be preserved;
- index impact: synchronized canonical indexes, or a concrete reason for
No index change.
Verification status is explicit:
| Status | Meaning |
|---|---|
| Passed | the check actually ran and produced the expected evidence |
| Failed | the check ran and diverged from the contract |
| Blocked | a required gate lacks authorization, budget, credentials, recovery evidence, or an external condition |
| Not run | the check was not performed, with a concrete reason and impact |
“Tests passed” without a command, scope, and result is not evidence. A check that did not run cannot become implied success through vague wording.
This design is particularly valuable for Vibe Coding. A later session can read the changelog and learn which requirements belong to the version, which evidence exists, and which risk remains. A tester no longer reconstructs acceptance scope from the diff; they verify the requirement-to-change-to-evidence map.
Released entries are retained historical facts. Current work does not rewrite them to make the history cleaner. New behavior moves forward through a new version and explicit superseded or compatibility language.
Version, index, and rollback discipline make changes locatable
A version gives one complete engineering milestone a stable identity. Index review prevents a new file, route, API, migration, or responsibility from existing only in its author’s memory.
Every handoff asks whether an indexed item was added, removed, renamed, moved, or reassigned, or whether an index description is now inaccurate. If so, every affected canonical index changes in the same milestone.
Rollback is more than git revert. An ordinary change identifies the specification, implementation, tests, version, and changelog that move together. A database migration additionally needs an exact target, immutable backup or tested forward-recovery path, objective recovery triggers, post-apply checks, and application compatibility. Missing recovery evidence blocks execution.
Only then does Vibe Coding move from “code appeared in a workspace” to “the engineering system contains an identifiable, acceptable, and reversible change.”
The eight lines I now give a Coding Agent
Instead of “add this feature,” I start with:
1. Specification admission: which permanent contract owns it, and does history conflict?
2. User outcome: what can the user do when this is complete?
3. Domain owner: which domain and named use case own it?
4. Invariants: which facts must remain true?
5. State authority: who owns truth and who holds projections?
6. Failure/concurrency: how does it cancel, retry, roll back, and recover?
7. Acceptance evidence: which tests and real checks prove completion?
8. Delivery record: how do version, changelog, indexes, and rollback stay synchronized?
Then I ask the coding agent to state the call chain, dependency direction, and affected boundaries before editing.
Compare “add renaming to the strategy list and show an error if it fails” with this:
Add the
rename owned strategyuse case to the Strategy domain. PostgreSQL owns the display name. The browser may project it optimistically but keeps a confirmed snapshot and request generation. The Repository maps the Action. The Store owns immediate state, server acknowledgement, conflict reload, failure rollback, and later-input preservation. The View only renders and emits rename intent. Cover out-of-order responses, revision conflict, dependency failure, and destroy cancellation.
The second prompt may not produce less code. It produces fewer invented rules.
This does not mean five layers for every button
Domain-driven does not mean directory-driven. Hover, animation, disclosure, and visual layout with no business meaning can remain local to the component.
I use one question: Would losing this state change business meaning, a user decision, an async result, or another instance’s behavior?
- If not, it is probably presentation state.
- If yes, it needs a domain owner and authority.
- If it coordinates requests or instances, it cannot live only in a browser or process.
- If Web, Agent, Worker, and Socket share the rule, the rule belongs in a shared Application Service or domain port.
The goal is not maximum separation. The goal is one explainable, testable path for every meaningful change.
Vibe Coding is limited by what the system can reject
AI is excellent at completing local implementations. It does not automatically know which local choice damages the larger system. Stability comes from an architecture that rejects business workflows in Views, realtime as truth, queue-before-persistence, memory-based Worker recovery, hidden dependency failure, and duplicated rules across entry points.
In this context, domain-driven design is not a grand vocabulary. It is a short set of questions:
- Who owns this behavior?
- Where is truth stored?
- Which boundary may change it?
- What does recovery read after failure?
- Can we prove it without rendering the entire system?
When those answers are stable, Vibe Coding becomes more than fast code generation. It becomes fast delivery of a product that can continue to evolve.
Appendix: give the method to your Coding Agent
I packaged this engineering method as an installable Codex Skill. It includes the full delivery loop plus focused references for specification/changelog delivery, frontend state, asynchronous runtimes, and verification:
stable-vibe-coding/
├── SKILL.md
├── agents/openai.yaml
└── references/
├── change-delivery.md
├── frontend-state.md
├── async-runtime.md
└── verification.md
Download the stable-vibe-coding Skill (ZIP)
Invoke it with:
Use $stable-vibe-coding to take this change through specification admission,
explicit architecture, verification, and changelog evidence.
The Skill does not choose product direction for you. It makes a coding request pass specification admission, then answer domain ownership, state authority, failure recovery, acceptance evidence, and changelog traceability.