← devamhatre.cloud

"The question you ask first determines the architecture you end up with. Change the question — and the entire structure changes with it."

One question separates how a junior developer thinks from how a senior thinks.

It is not about knowing more patterns. It is not about years of experience. It is the question that fires first — before any file is opened, before any component is named.

Junior Developer Asks
"How do I build this feature?"
Senior Developer Asks
"What is always true about this feature, regardless of how I build it?"

Sit with these three questions until you can answer them on paper.

Not in code. On paper. If you cannot answer them in plain language, you do not understand the domain yet — and any code you write will need to be restructured when the understanding arrives.

  1. What is always true here?

    These are your invariants. Things that hold regardless of implementation. A teacher always assesses a student. Evidence is always collected. A result is always saved. These become your domain layer — your types, your contracts. The most stable layer of the system. It imports nothing from React, nothing from any framework.

  2. What differs across instances of this feature?

    These are your variation points. How evidence is captured differs. Which AI analyzes it differs. What the UI looks like differs. These become your discriminated union arms, your pluggable components — the parts that can change without touching the stable core. Adding a new variant touches zero existing files.

  3. What is the direction of dependency?

    The stable thing must never depend on the unstable thing. Domain must not import from React. Use case hooks must not import components. If the most stable layer depends on the least stable, every sprint that changes the UI breaks the core. Dependencies always point inward — toward stability.

Four questions that produce an architecture — not just a plan.

Fill this in before writing any code for a new feature. Not as a formality — as a design act. If any quadrant is blank, the feature is not ready to build yet. You are missing understanding, not code.

Feature Design Template
What is always true?
Invariants → Domain layer (types, contracts)
What varies?
Variation points → Discriminated union arms
What orchestrates the rules?
Business logic → Use case hook
What just displays?
Render only → UI component

Before putting logic in a component, ask: would I keep this if we rebuilt in Flutter?

If yes — it belongs in the use case layer. It is business logic, and it belongs in a hook that can be tested with renderHook and zero native mocks. If no — it is display logic, and it belongs in the UI layer.

// ❌ Fails the Flutter test — UI makes a decision
if (isFormValid && !isSubmitting) {
  submitAssessment();    // component is deciding when to submit
}

// ✅ Passes the Flutter test — UI reports; use case layer decides
onSubmitPressed();       // hook owns the guard logic, the timing, the ref

Every time you write a type, ask: is TypeScript enforcing this, or am I relying on discipline?

Discipline fails. TypeScript does not forget. If correctness depends on every developer always remembering to check a nullable field, that check will eventually be missed — at 2am, in production, on a student's record.

// ❌ Discipline-based — flat bag. TypeScript cannot help here.
type CollectedEvidence = {
  audioUrl?:      string | null
  imageUrl?:      string | null    // ~40% of fields set by each consumer
  selectedLevel?: Level  | null    // the rest are silently undefined
  durationSec:    number
}

// ✅ Enforcement-based — discriminated union. TypeScript catches the bug.
type CollectedEvidence =
  | { type: 'audio';       audioUrl: string;     durationSec: number }
  | { type: 'photo';       imageUrl: string;     durationSec: number }
  | { type: 'teacher';     selectedLevel: Level; durationSec: number }
  | { type: 'interactive'; selectedLevel: Level; durationSec: number }

A discriminated union where each arm is complete means TypeScript catches the bug — not QA, not a toast in production, not a teacher whose student was graded on a stale value.

They are not smarter. They have been burned more times.

Architecture is not cleverness. It is accumulated scar tissue, written down so the next person does not get the same wound.

Common anti-patterns and their real-world consequences
The shortcut taken What actually happened
Flat nullable bag Bug at 2am because one consumer didn't fill the field everyone assumed was always set. Manual DB fix. Embarrassment.
Business rule in a component Fixed in 2 of 3 places. The third crashed a real user's session three months later during a demo.
useState as a submit guard Rapid double-tap wrote the same record twice on the same student. Support ticket. Manual cleanup.
|| on a numeric field Valid 0 threshold silently became null. The backend applied its own default. Wrong students were promoted for weeks.
No response.ok check HTTP error silently parsed as JSON. Garbage written to the DB. No error in the logs. Found during an audit.
reject(string) in Promises The actual backend error reason was discarded at the callsite. Teachers saw a generic toast. Nobody knew why saves were failing.