Boundary rule
Keep your app code speaking your app’s language. Put outside shapes, SDK calls, SQL, Firebase code, and payment responses at the edge.
Adapter changes an external interface into one your app expects. Repository gives one place to read and write domain data.
Why this matters
Hackathon apps often start with API calls inside button handlers or routes. It works until you change from a mock API to Supabase, MongoDB, Firebase, or a college server.
If the boundary is hidden, your core code stays stable. You can swap the outside part without rewriting the feature.
Adapter versus repository
Use an Adapter when some outside thing has the wrong shape. For example, a payment SDK returns amount in paise, but your app uses rupees.
Use a Repository when code needs stored data. For example, getUser, saveProject, listSubmissions. The repository hides whether data came from SQL, JSON, localStorage, or an API.
Small TypeScript example
Here the app wants a simple PaymentGateway. Razorpay or any other provider can have its own response shape behind an adapter.
The app service does not know provider fields. It only knows pay().
type PaymentResult = {
ok: boolean;
referenceId: string;
};
interface PaymentGateway {
pay(amountRupees: number): Promise<PaymentResult>;
}
class RazorpayAdapter implements PaymentGateway {
constructor(private razorpayClient: any) {}
async pay(amountRupees: number): Promise<PaymentResult> {
const response = await this.razorpayClient.createPayment({
amount: amountRupees * 100
});
return {
ok: response.status === "captured",
referenceId: response.id
};
}
}
class CheckoutService {
constructor(private payments: PaymentGateway) {}
async checkout(totalRupees: number) {
const result = await this.payments.pay(totalRupees);
if (!result.ok) throw new Error("Payment failed");
return result.referenceId;
}
}Repository shape
A repository should sound like your product, not your database tables. Keep query details out of routes and UI components.
Start with the methods your feature needs today. Do not build a full data layer for imaginary screens.
type Project = {
id: string;
title: string;
ownerId: string;
};
interface ProjectRepository {
findByOwner(ownerId: string): Promise<Project[]>;
save(project: Project): Promise<void>;
}
class ApiProjectRepository implements ProjectRepository {
constructor(private baseUrl: string) {}
async findByOwner(ownerId: string): Promise<Project[]> {
const res = await fetch(`${this.baseUrl}/projects?ownerId=${ownerId}`);
return res.json();
}
async save(project: Project): Promise<void> {
await fetch(`${this.baseUrl}/projects`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(project)
});
}
}Common mistake
Do not create one giant repository called AppRepository. That becomes a dumping ground.
Split by domain need: ProjectRepository, UserRepository, SubmissionRepository. If a method is only used by one screen and has no pain yet, plain code may be enough.
Use AI carefully
Ask AI to find boundary leaks: “List places where API, SDK, SQL, or localStorage details are mixed into business logic. Suggest adapter or repository names.”
Check the output. Reject wrappers that only rename one function and add no testability or swap benefit. Keep names close to your app domain.
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done