api-and-interface-design
Skill2 starsChanged 40 days agoMITView on GitHub
Use when designing interfaces between layers (Repository, UseCase, API client) or defining data contracts. Covers Retrofit interfaces, Room DAOs, Kotlin sealed classes, backward compatibility, and idempotency keys for safely retried writes.
72 other projects use a version of this.
Adopted by
Most recently taken up by
- ishandutta2007/Awesome-Agent-Skillstheir own version, first seen Nov 8, 2026
- DnsQueen/orcinus-skillstheir own version, first seen Nov 8, 2026
- TheAstroByte/astrobyte-agent-skillstheir own version, first seen Nov 8, 2026
- MMACABRE/SKILLStheir own version, first seen Nov 8, 2026
- steven123397/nexuskit-skillstheir own version, first seen Nov 1, 2026
- AgnesAI-Labs/agnet-skills-cntheir own version, first seen Nov 1, 2026
Used with
Projects with it also use brainstorm-experiments-existing, prioritize-assumptions and 4 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
skytiger6724Skillbrainstorm-experiments-existing
Design experiments to test assumptions for an existing product — prototypes, A/B tests, spikes, and other low-effort validation methods. Use when validating assumptions, testing feature ideas cheaply, or planning product experiments.
Also in 4 projects that use this
skytiger6724Skillprioritize-assumptions
Prioritize assumptions using an Impact × Risk matrix and suggest experiments for each. Use when triaging a list of assumptions, deciding what to test first, or applying the assumption prioritization canvas.
Also in 4 projects that use this
45ckSkillpestle-analysis
Also in 4 projects that use this
About-IntelligenceSkillpositioning-ideas
Brainstorm product positioning ideas differentiated from competitors.
Also in 4 projects that use this
- +2 more from skytiger6724
Other versions72 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 72
- addyosmani/agent-skills★ 114ksame name
- ollaya-dev/ollaya★ 1.4ksame name
- shashankswe2020-ux/whoop-mcp★ 174same name
- drvoss/everything-copilot-cli★ 48same name
- vinvcn/addyosmani-agent-skills-zh★ 46same name
- jmxt3/gitscape.ai★ 33same name
- sampleXbro/agentsmesh★ 27same name
- jaskirat1616/grok-skills★ 22same name
- ishandutta2007/Awesome-Agent-Skills★ 21same name
- JPeetz/agent-skills★ 14same name
62 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- android-accessibilitySkill
- android-architectureSkill
- android-background-workSkill
- android-data-persistenceSkill
- android-device-testingSkill
- android-e2e-verificationSkill
- android-ui-engineeringSkill
- ci-cd-and-automationSkill
- code-review-and-qualitySkill
- code-simplificationSkill
- constraint-driven-developmentSkill
- context-engineeringSkill
- debugging-and-error-recoverySkill
- deprecation-and-migrationSkill
- documentation-and-adrsSkill
- doubt-driven-developmentSkill
- git-workflow-and-versioningSkill
- idea-refineSkill
- incremental-implementationSkill
- interview-meSkill
- observability-and-instrumentationSkill
- performance-optimizationSkill
- planning-and-task-breakdownSkill
- security-and-hardeningSkill
- shipping-and-launchSkill
- source-driven-developmentSkill
- spec-driven-developmentSkill
- test-driven-developmentSkill
- using-agent-skillsSkill
The file
We split this skill into the rules it gives an agent
8 rules. 0 are common, 3 are distinctive, and 5 are very specific to this project.
0
Common
Most of the 67 edited versions we read keep these.
3
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 48
Use suspend for one-shot operations
- Very specific: line 82
Use @Serializable data classes for request/response bodies (Kotlin Serialization)
- Very specific: line 84
Use sealed class for polymorphic responses
- Very specific: line 85
Never expose Retrofit internals (no Response<T> in Repository interface)
- Distinctive: line 152
Never swallow errors silently
- Distinctive: line 180
Update all callers in the same PR
- Very specific: line 258
Don't derive the key from content ("$userId:$amount") — two legitimate identical orders would collapse into one.
- Distinctive: line 279
No validation at system boundaries
“Common” is measured: kept by at least 6 in 10 of the 67 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
API and Interface Design
Overview
Hyrum's Law: "With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody." Design interfaces — Repository contracts, UseCase signatures, Retrofit services, Room DAOs — as deliberate contracts. Everything observable becomes a dependency.
When to Use
- Defining a new Repository interface
- Creating Retrofit API service interfaces
- Designing Room DAOs
- Defining ViewModel ↔ UI contracts (UiState, Events)
- Changing any public interface in a shared module
- Designing inter-module contracts in multi-module projects
- Sending state-changing requests that can be retried (payments, orders, messages, uploads)
Skip when: Modifying internal (private) implementation details.
Core Process
Step 1: Contract-First Design
- Define the interface before implementing:
// Repository contract (in :core:domain)
interface TaskRepository {
fun getTasks(): Flow<List<Task>>
fun getTaskById(taskId: String): Flow<Task?>
suspend fun createTask(title: String, description: String?): Task
suspend fun updateTask(task: Task)
suspend fun deleteTask(taskId: String)
suspend fun syncWithRemote()
}- Design principles:
- Return
Flowfor observable data (notsuspendfor single reads) - Use
suspendfor one-shot operations - Accept domain types, not framework types (no
Entityin interfaces) - Throw domain-specific exceptions, not framework exceptions
- Return
Step 2: Retrofit Interfaces
- Define clean API contracts:
interface TaskApi {
@GET("tasks")
suspend fun getTasks(
@Query("page") page: Int = 1,
@Query("per_page") perPage: Int = 20,
): TaskListResponse
@GET("tasks/{id}")
suspend fun getTask(@Path("id") taskId: String): TaskResponse
@POST("tasks")
suspend fun createTask(@Body request: CreateTaskRequest): TaskResponse
@PUT("tasks/{id}")
suspend fun updateTask(
@Path("id") taskId: String,
@Body request: UpdateTaskRequest,
): TaskResponse
@DELETE("tasks/{id}")
suspend fun deleteTask(@Path("id") taskId: String)
}- API contract rules:
- Use
@Serializabledata classes for request/response bodies (Kotlin Serialization) - Separate request and response models (even if fields overlap)
- Use
sealed classfor polymorphic responses - Never expose Retrofit internals (no
Response<T>in Repository interface)
- Use
Step 3: Data Models and Sealed Types
- Use the type system to make illegal states unrepresentable:
// BAD: stringly-typed status
data class Task(
val id: String,
val title: String,
val status: String, // "pending", "in_progress", "completed" — typos possible
)
// GOOD: sealed type
data class Task(
val id: String,
val title: String,
val status: TaskStatus,
)
sealed interface TaskStatus {
data object Pending : TaskStatus
data object InProgress : TaskStatus
data class Completed(val completedAt: Instant) : TaskStatus
}- Use value classes for type safety:
@JvmInline
value class TaskId(val value: String)
@JvmInline
value class UserId(val value: String)
// Prevents accidentally passing a UserId where a TaskId is expected
suspend fun getTask(taskId: TaskId): TaskStep 4: Error Handling Strategy
- Define domain-specific errors:
sealed interface DataError {
data class Network(val cause: Throwable) : DataError
data class NotFound(val id: String) : DataError
data class Validation(val message: String) : DataError
data object Unauthorized : DataError
data class Unknown(val cause: Throwable) : DataError
}
// Use Result type for operations that can fail
suspend fun createTask(title: String): Result<Task>
// Or define a custom Result type
sealed interface DataResult<out T> {
data class Success<T>(val data: T) : DataResult<T>
data class Error(val error: DataError) : DataResult<Nothing>
}- Error handling rules:
- Catch specific exceptions, not generic
Exception - Map framework exceptions to domain errors at the boundary
- Never swallow errors silently
- Provide actionable error messages for the UI
- Catch specific exceptions, not generic
Step 5: Backward Compatibility
- Extend, don't modify:
// Version 1
interface TaskRepository {
fun getTasks(): Flow<List<Task>>
suspend fun createTask(title: String): Task
}
// Version 2: ADD new methods, don't change existing signatures
interface TaskRepository {
fun getTasks(): Flow<List<Task>>
suspend fun createTask(title: String): Task
// New: added with default implementation for backward compatibility
fun getTasksByStatus(status: TaskStatus): Flow<List<Task>> =
getTasks().map { tasks -> tasks.filter { it.status == status } }
}- When breaking changes are necessary:
- Deprecate with
@Deprecatedand replacement guidance - Provide a migration period
- Update all callers in the same PR
- See
deprecation-and-migrationfor the full process
- Deprecate with
Step 6: Naming Conventions
| Element | Convention | Example |
| Repository | get*, observe*, create*, update*, delete* | getTaskById() |
| UseCase | Verb phrase, operator fun invoke() | GetTasksUseCase() |
| Retrofit | HTTP-verb aligned | @GET, @POST, @PUT, @DELETE |
| Room DAO | observe* for Flow, get* for suspend | observeAll(), getById() |
| UiState | Sealed interface per screen | TaskListUiState.Success |
| Events | Past-tense or imperative | TaskClicked, DeleteTask |
| Request/Response | Suffixed | CreateTaskRequest, TaskResponse |
Step 7: Validation at Boundaries
- Validate at system boundaries, trust internally:
// At the API boundary (Retrofit interceptor or repository)
class TaskRepositoryImpl @Inject constructor(
private val api: TaskApi,
private val dao: TaskDao,
) : TaskRepository {
override suspend fun createTask(title: String, description: String?): Task {
// Validate at boundary
require(title.isNotBlank()) { "Task title must not be blank" }
require(title.length <= 200) { "Task title must not exceed 200 characters" }
val entity = TaskEntity(
id = UUID.randomUUID().toString(),
title = title.trim(),
description = description?.trim(),
)
dao.upsert(entity)
return entity.toDomain()
}
}Step 8: Idempotency Keys for Retried Writes
- Assume every write is sent more than once. On Android, duplicates come from OkHttp's
retryOnConnectionFailure, caller retry loops, WorkManager retries with backoff, double-taps, and process death between sending a request and persisting the response. A timeout means unknown, not failed — the server may already have applied it. For non-idempotent writes (POSTthat charges, orders, sends), attach anIdempotency-Key.
Key the intent, not the attempt. Generate a UUID once when the user commits the action, persist it with the pending operation (Room outbox row or WorkManager inputData), and reuse it on every retry — including after process death:
@Entity data class PendingOrder(@PrimaryKey val idempotencyKey: String, val cartId: String, val payload: String)
suspend fun placeOrder(cart: Cart) { // runs once per user intent
val op = PendingOrder(UUID.randomUUID().toString(), cart.id, json.encodeToString(cart.toRequest()))
outboxDao.insert(op) // key survives process death
workManager.enqueue(OneTimeWorkRequestBuilder<SubmitOrderWorker>()
.setInputData(workDataOf("key" to op.idempotencyKey)).build())
}
interface OrderApi {
@POST("orders")
suspend fun createOrder(@Header("Idempotency-Key") key: String, @Body body: CreateOrderRequest): OrderResponse
}
// In SubmitOrderWorker.doWork(): same key on every attempt
val key = inputData.getString("key") ?: return Result.failure()
val op = outboxDao.get(key) ?: return Result.success() // already submitted
return try {
api.createOrder(op.idempotencyKey, json.decodeFromString(op.payload))
outboxDao.delete(op)
Result.success()
} catch (e: IOException) {
Result.retry() // outcome unknown: retry with the same key
} catch (e: HttpException) {
if (e.code() == 409) Result.retry() else Result.failure() // 409 = still in flight (check your server's contract)
}- Never generate the key inside an OkHttp interceptor, the Retrofit call, or
doWork()— that layer runs once per attempt. If you prefer an interceptor over@Header, pass the stored key via a Retrofit@Tagand read it withrequest.tag(). - Don't derive the key from content (
"$userId:$amount") — two legitimate identical orders would collapse into one.
Know the server contract (usually another team's): it stores key + request fingerprint + response, replays the stored response for a repeat, rejects the same key with a different body (422, sometimes 409), returns 409 while the first attempt is still in flight, and expires keys after a window. Client consequences: treat a replayed response as success, retry an in-flight 409 with backoff, treat a body-mismatch error as a client bug (never "fix" it with a new key), and keep the outbox retry horizon shorter than the server's key expiry.
Common Rationalizations
| Shortcut | Why It Fails |
| "I'll define the interface from the implementation" | Implementation leaks into the contract. Design the contract first. |
| "One model for all layers is simpler" | Coupling DB schema to API to UI makes all three brittle. |
| "Strings are fine for types" | Strings allow typos, have no exhaustiveness checking, and no IDE support. |
| "We don't need backward compatibility yet" | By the time you need it, you have callers you can't easily find. |
| "Duplicate requests are rare" | They're correlated with bad networks and degraded backends — exactly when retries spike and duplicates cost most. |
| "A new key per retry is fine, the server dedupes" | The server dedupes by key. A fresh key per attempt is a fresh charge per attempt. |
| "The key can live in memory" | Process death mid-request is routine on Android. A key that isn't persisted with the operation is lost exactly when a retry is needed. |
Red Flags
- Room entities exposed to UI layer
- Retrofit
Response<T>returned from Repository - Generic
Stringwhere sealed type or enum is appropriate - No validation at system boundaries
- Breaking interface changes without deprecation
AnyorObjectin public interfaces- Inconsistent naming across layers
- Non-idempotent
POST(payment, order, message) retried without anIdempotency-Key UUID.randomUUID()for the key inside an interceptor, API call, ordoWork()- Idempotency key held only in memory or a ViewModel, not persisted with the pending operation
- A
409/422on a keyed request "handled" by retrying with a new key
Verification
- Interfaces designed before implementation (contract-first)
- Domain types used at interface boundaries (not framework types)
- Sealed types used for finite state sets
- Error handling uses domain-specific error types
- Validation at system boundaries, trust internally
- Naming follows project conventions
- Backward compatibility maintained (or breaking change documented)
- Tests verify interface contracts
- Every retryable state-changing request sends an
Idempotency-Key, or is documented as unsafe to retry - Key is generated once per user intent and persisted with the pending operation (test: kill the process mid-request, the retry sends the same key)
409/422and replayed responses are handled per the server contract, and the client retry horizon is shorter than server key expiry
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

