---
title: "Your AI Coding Agent Still Needs an Architecture"
description: "Use explicit contracts, behavior tests, and dependency checks to guide AI coding agents and review their changes through a TypeScript favorites example."
url: https://www.callstack.com/blog/your-ai-coding-agent-still-needs-an-architecture
updated: 2026-10-24T08:39:39.538Z
---

# Your AI Coding Agent Still Needs an Architecture

Use explicit contracts, behavior tests, and dependency checks to guide AI coding agents and review their changes through a TypeScript favorites example.

_By Szymon Chmal, Software Developer @ Callstack · Published 2026-10-22 · Category: AI_

Imagine an article app on your phone. You tap the heart just as your train enters a tunnel, and the connection drops. Later, the article is missing from your favorites.

Your team gives an AI coding agent a request:

_Add offline support for favorites. A change made without a connection should survive an app restart and synchronize when the connection returns._

The failure was reported on the article screen, so the agent starts there. It may discover that the feed has another favorite button and the favorites page has a remove action, but the report does not identify those paths.

Each screen handles its own requests and state, so an agent that sees only one path can deliver a convincing fix that leaves the other paths untouched.

The problem does not end when the feature ships. A week later, someone might report that the heart on the article screen turns off when synchronization fails. An agent working from that report may fix the article screen and stop. The feed has its own favorites logic, so fixing the article screen may leave the same bug in the feed.

Good architecture makes the expected change easier for an agent to find and harder to apply inconsistently. A clear favorites boundary tells it where saving, failure, and retry behavior belongs. A stable contract and tests tell the team whether the result meets the requirement.

This does not guarantee that an agent will find every caller, but it gives engineers a firmer basis for trusting and reviewing the work. This article follows that boundary through migration, tests, and a later bug fix.

## Share favorites behavior across screens

A common agent refactor would extract the API calls from these three screens into a helper and reuse it. That removes a duplicated request, but the decision about what happens next can remain in each screen.

In the article screen, a button handler might call the helper, update in-memory state, and display an error if the request fails. The feed has another favorite button, and the favorites page has a remove action. Each can still respond differently to success, failure, or a later retry.

![](https://www.callstack.com/assets/6ada03bf73eef016ebdd05d9_01-target%403x-2.png)

Adding offline support means revisiting every write path. Each must save the user's choice locally, keep it after a restart, and synchronize it when connectivity returns. A screen that skips any step may appear correct during an ordinary online test and fail in the tunnel.

Separate implementations also let error handling drift.

- The article screen might undo the selected heart after a network failure.
- The feed might keep it selected.
- The favorites page might remove the item until the next reload.

An agent can make any one of those screens look fixed without resolving the disagreement. The article may appear saved on one screen and unsaved on another.

![](https://www.callstack.com/assets/6ada03dcf1df9118354c501f_01-current%403x-2.png)

A repository gives all three screens one place to read and update favorites. The feed, article screen, and favorites page still own their interactions and presentation, but they call the same favorites API. The repository owns the rules for reading and changing favorites, and the team can specify and test those rules once.

## Give favorites a contract the agent can work within

The repository pattern gives callers an interface for the favorites data they need, without exposing how that data is obtained or saved. For this feature, its public interface could be:

The article screen saves an article through that entry point:

The favorites page calls the same method with `false` to remove it. Neither screen needs to know whether the repository currently calls a server, writes to a local database, or queues work for later. We can change those details behind the interface as long as we preserve its promised behavior. Offline support changes how favorites are stored. The repository interface lets each button save or remove a favorite without needing to know how synchronization works.

An interface alone is not the architecture. The team has to decide what its methods mean, make callers use them, and keep the implementation behind the boundary. If screens still interpret network failures themselves, the most consequential favorites behavior remains duplicated.

Now return to the offline support request. A tap made without a connection must survive an app restart, even though the server cannot confirm it yet. That changes what `setFavorite` can promise when it resolves.

In this design, success means **saved on the device**. The method resolves after the selected state and a pending synchronization record are committed together to local storage. If that local write fails, it rejects. `getAll` reads the persisted selection, so the favorite remains visible after a restart. A later network failure leaves the saved choice pending rather than silently reversing it.

Consider the later report from the article screen: the heart turns off after synchronization fails. The contract gives the agent a way to investigate it.

- If favorites data was rolled back despite a successful local save, the fix belongs in the repository's behavior.
- If the data remains saved but the article screen renders it incorrectly, that screen needs a presentation fix.

The report identifies where the problem appeared, and the contract helps locate its cause.

A sync worker retries pending changes when the app can connect. Rapid taps also need an ordering or conflict policy so an old response cannot overwrite the user's latest choice. Those policies should be explicit.

Otherwise, an agent may settle them differently in each screen or make a local fix that fails under a later request.

Changing the meaning of success affects existing callers. The current online code may treat a resolved request as server confirmation. During migration, the agent must find callers that show "synced" or take another action on that assumption. The two-method example above does not provide synchronization status, so a pending indicator may require a separate status API or UI state.

## Move every caller in reviewable steps

Defining the repository contract does not make the existing screens use it. In our starting app, the feed, article screen, and favorites page still make their own API calls and update their own state. To deliver the offline support request through one boundary, the agent has to move each of those callers.

It could do that and add offline synchronization in one broad change, but a regression would then be hard to trace to the migration or the new behavior.

Start by wrapping the existing online behavior in the repository. Move one caller behind it and verify that the screen behaves as before. Then migrate the others. A first task could be:

_Introduce_ `FavoritesRepository` _with the current online behavior. Move the feed's favorite handler behind it. Preserve the feed's behavior and verify it with the relevant tests. Leave the article screen and favorites page for later migration steps._

The feed handler should get shorter as its favorites logic moves into the repository. Tests, dependency wiring, and exports may need edits too. Review should establish that the feed delegates the operation and still behaves as before.

Keep an inventory of every action that changes a favorite: adding from the feed, adding from an article, and removing from favorites. Search for direct writes, then trace the user actions. A search with no remaining direct API calls can still miss a write hidden behind a helper. Name any temporary exceptions so unfinished migration work stays visible.

One or two more small commits move the article screen and favorites page behind the repository. With all three callers using it, we can add local persistence and synchronization, then verify the result in one place.

## Test favorites without the screens

Before adding offline support, we need tests that show whether a favorite survives a failed request and an app restart.

A button test may pull in navigation, third-party UI components, a global store, an HTTP client, storage, and connectivity state. To keep the setup manageable, an agent may mock many of them. That can be useful for a presentation test, but the feed and article tests may each end up with their own fake favorites hook.

As the contract changes, those copies can drift from the implementation and from each other. A test that supplies a selected article to the hook shows that the heart renders as selected. It cannot show that the selection survives a failed sync or restart.

Moving logic to a repository helps only if its dependencies can be controlled. A repository that constructs its own database connection and HTTP client can be nearly as hard to test as the screen handlers.

Instead, describe the storage and remote operations it needs through interfaces. Production adapters use the app's database and HTTP client. Tests replace those adapters with implementations that let us control storage results and network responses.

![](https://www.callstack.com/assets/6ada11f299202e015ba3f71b_02-testing-boundaries%403x-2.png)

This is the idea behind [ports and adapters, also called hexagonal architecture](https://alistair.cockburn.us/hexagonal-architecture). The public repository interface shields screens from storage and synchronization. Inside that boundary, storage and remote ports describe the external operations the favorites behavior needs. Adapters implement them for a particular database and HTTP client.

Application setup supplies the production dependencies:

The same wiring lets a test replace the adapters with controlled versions. Here is an illustrative Vitest test. Its storage adapter keeps state in memory; the remote adapter fails on command.

The worker's runOnce method attempts pending changes and leaves a failed change queued:

The test controls storage and the remote response without mocking the repository's decisions. In a larger suite, the in-memory adapter should be a shared test fixture rather than a copy in every test.

A separate storage integration test should verify that the real adapter commits the selection and pending record atomically and retains them after restart. A delayed-response test can check that an older request does not replace the latest local choice. Preserving that choice on the server still requires the chosen ordering or conflict policy.

These tests give the agent a fast loop and give reviewers evidence about shared behavior.

- UI tests can check how each screen responds to a tap or displays an error.
- Storage and app tests should verify that the actual implementation persists state.

A mocked hook alone cannot establish either guarantee.

The boundary should stay proportional to the feature. Ordinary interfaces and function arguments are enough for this example. A generic repository framework or event bus needs a requirement that justifies the added complexity.

## Make the boundary visible to the next agent

After migration, a screen could still import storage directly and create a new favorites write path. Tests of the repository might keep passing while that screen diverges. Put the import rule into an automated dependency check so a violation fails during agent work and in CI.

`AGENTS.md` can explain the repository-specific rule to coding agents. [agents.md](https://agents.md/) describes the format. Even a capable agent can miss that guidance, and a less capable model may be more likely to do so. An automated check catches the forbidden import regardless of which model wrote it.

Feature modules often define their public API through an `index.ts` file. In this example, screens should import favorites capabilities from that file. A deep import into a storage or synchronization file bypasses the module's intended contract. [Dependency-cruiser](https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md) can enforce that boundary. This illustrative rule belongs in its `forbidden` array:

The rule covers one boundary in one illustrative folder structure. Other rules may be needed to keep favorites logic from importing UI code or concrete adapters, and to stop neighboring features from reaching into its internals. The allowed `index.ts` exports need review as well. If it re-exports a storage adapter, a screen can import that adapter without breaking the rule and still take direct control of favorites persistence.

Verify the configuration by deliberately adding a forbidden import and confirming that the check fails. Account for the project's aliases and module resolution so alternate import paths are covered.

## Build an environment agents can change safely

Developers are responsible for the environment in which agents work. In this example, they decide what the favorites module promises, expose that promise through its public API, and prevent screens from reaching into its storage code. Tests and dependency checks make those boundaries visible to an agent while it works and to engineers reviewing its patch.

Developers also look ahead to likely changes. Offline support means persistence and synchronization may evolve independently of the buttons that use them. The repository interface protects those callers, while the storage and remote ports give the implementation places to change without a screen-wide refactor. This request already calls for local persistence and synchronization, so the interfaces have a concrete job.

An agent may write most of the next implementation, but the team still has to shape how the system can evolve. When a new requirement does not fit the existing contract, engineers should revisit that contract deliberately rather than ask the agent to work around its checks.

## Let the next request test the decision

After offline favorites ships, the team asks the agent to use exponential backoff for failed synchronization attempts. Each retry should wait longer than the last, so the app does not repeatedly send requests at the same short interval while the connection is down.

![](https://www.callstack.com/assets/6ada124a7926e140f7bb95d3_03-change-scope%403x-2.png)

If retry behavior lives in the feed, article screen, and favorites page, the agent must find and change three implementations and their tests. Missing one path leaves that screen on the old retry schedule. With synchronization behind the repository boundary, the expected patch is smaller:

_Add exponential backoff to favorites synchronization. Keep the_ `FavoritesRepository` _contract unchanged. Test the retry delays and confirm that a failed attempt leaves the local favorite and its pending change intact. The feed, article screen, and favorites page should continue to use the same API._

The synchronization implementation and its tests should carry most of this change. UI callers are expected to remain untouched because they do not schedule retries. If the agent needs to edit a screen to change the retry policy, reviewers have a concrete reason to inspect that dependency.

## Back on the train

The next tap in the tunnel saves the selection and its pending synchronization record on the device. The article remains in favorites after the app restarts. When connectivity returns, synchronization resumes under the ordering policy the team defined.

Weeks later, a bug report names one screen. The agent can reproduce the failure against the shared contract, then change the behavior or presentation where the cause lives. Reviewers can check that the other favorite actions remain consistent without comparing three separate implementations again. That is why architecture still matters when an agent writes the patch: it defines where a decision belongs and gives the team a way to verify the next change.

**Further reading:** See [how we used agents to deliver a web platform](https://www.callstack.com/case-studies/building-an-agentic-sdlc-for-a-web-launch-in-under-one-month), including how we scoped tasks and kept engineers responsible for architecture and final approval. If your team is replacing shortcuts that no longer fit the product, explore [our services](https://www.callstack.com/services/ai-vibe-coding-cleanup).
