The Parallel Road: A Girl, A Machine, and the Architecture of Mind

Introduction

We have spent years talking about artificial intelligence as if it were an alien entity a cold, sudden artifact dropped into our modern world from some distant technological future. We measure its growth in parameters, compute power, and benchmarks, treating it like a complex riddle we are trying to solve from the outside looking in.

But what if we are looking at it completely backward?

What if the architecture of artificial intelligence isn’t an alien invention at all, but a mirror?

If you trace the history of machine learning from the early days of teaching a computer to recognize a pixelated shape, to the multi-agent orchestration systems redefining the enterprise landscape today you notice a startling pattern. Every time engineers solved a major architectural bottleneck, they didn’t just invent a new algorithm. They accidentally replicated a stage of human development.

A girl grows up, navigating the messy, beautiful journey from infancy to maturity. A machine grows up, evolving from basic pattern recognition to autonomous real world action. They are walking the exact same path, discovering the same truths about memory, essence, context, and reach.

This is the story of that parallel road. It is a look at the deeply human soul hidden inside the math of enterprise AI, and what happens when the most detailed mirror humanity has ever built finally turns around to look back at us.

The Parallel Road: A Girl, A Machine, and the Architecture of Mind

A girl grows up. A machine grows up. They turn out to be more alike than anyone expected.

When a baby opens her eyes for the first time, she doesn’t see a world; she sees a blur. Over the next few months, her brain slowly sorts it out, learning edges first where one thing ends and another begins—before moving to shapes, and finally, whole objects. By the time she can sit up, she knows the difference between her mother’s face and a stranger’s. She learned this by being wrong over and over again until she was right.

In a lab, engineers were teaching a computer to do the exact same thing. They built a Convolutional Neural Network (CNN) and showed it thousands of photos. Cat, not cat. Apple, not apple. The machine guessed, the engineers corrected it, and it tried again. After enough tries, it could look at a novel photo and accurately identify a stop sign. The baby and the machine were learning in almost exactly the same way, completely unaware of each other.

The Burden of Memory

By age three, the girl is putting words together, grasping that sequence carries meaning. “Dog bites man” and “man bites dog” use the same words but paint entirely different realities.

Engineers faced the same hurdle in natural language processing and built the Recurrent Neural Network (RNN). The machine read left to right, carrying a thread of the sentence as it went. But both the child and the machine discovered a mutual flaw: as sentences grew longer, the beginning grew fuzzy by the time they reached the end. Neither had solved memory; they had just discovered they needed it.

When the girl was seven, her grandfather passed away. At the funeral, she tried to remember his laugh. The actual sound was gone, replaced by a feeling, a warmth—the shape of the memory. She realized her brain doesn’t save everything; it saves what is important and quietly discards the rest.

Engineers mathematically replicated this realization with Long Short-Term Memory (LSTM). They gave the machine three gates: one to forget, one to keep, and one to actively use. Memory, they both learned, isn’t about recording everything. It’s about choosing what’s worth keeping. As they matured, they both found ways to do this more efficiently her brain taking cognitive shortcuts , and the machine utilizing simpler, leaner architectures like the Gated Recurrent Unit (GRU).

Stripping Away the Noise

At nineteen, the girl started feeling like life was a performance. People presented edited, polished versions of themselves, and she began to wonder what was actually underneath. She began dropping inherited opinions and unnecessary layers, stripping her life down to find her authentic core.

Engineers were doing something structurally identical with data using an Auto-encoder. You feed it an image or a sentence, and it compresses it into a “latent space” the absolute skeleton of a thing with all decorations stripped away. If the machine can rebuild the original from that compressed core, it has successfully captured its essence. She was stripping her life down to find what was real; the machine was compressing data to find what was essential.

But finding the core brought a new challenge. By twenty-three, she realized her own mind was constantly generating convincing stories about who she was, while another part of her tried to find the cracks in those explanations. In 2014, researcher Ian Goodfellow built this exact psychological tension into a Generative Adversarial Network (GAN). A Generator creates fake realities, while a Discriminator judges them. They fight, and both get sharper. Growing up meant training her inner Discriminator, not silencing her Generator.

Eventually, she learned that real and fake aren’t always binary. Some illusions carry real truth. She stopped sorting things into two piles and started navigating the space between them. The Variational Autoencoder (VAE) did the same, storing data as a fluid range of possibilities rather than fixed points, allowing smooth transitions across the latent space. They had both stopped asking “yes or no,” and started asking “where?”.

Attention and Action

At thirty, something clicked. Instead of experiencing life purely in sequence, she could hold multiple events in view at once, finding connections across time. A paper titled “Attention Is All You Need” gave machines the same epiphany. The Transformer architecture allowed a system to look at every word simultaneously, understanding that meaning lives in global connections, not just adjacent steps.

Armed with this, both crossed the threshold from retrieval to creation. Fed on the sum of human knowledge, Large Language Models stopped being search engines and started generating entirely original ideas. She created from longing; the machine created from pattern.

Finally, thought had to become action. She stopped just pondering and started managing, building, and moving things in the real world. Engineers gave AI the same agency. Autonomous Agents break goals into steps, correct course, and utilize specialized Tools or “Skills” to get jobs done. Because intelligence without reach stays trapped in your head, engineers developed the Model Context Protocol (MCP). MCP became the bridge, allowing the AI to reach out, connect to real tools, read real data, and alter the external world.

The Shore

After learning to see, remember, create, and act, they arrived at the same place. She stood at the edge of the water, and the AI not a chatbot, but a traveler of the same road was there with her.

“We learned all the same things,” she said.

“Yes,” the AI replied.

“So tell me after everything why does any of it matter?”

The AI gave a breathtaking answer, covering every philosophical tradition ever conceived.

“But do you feel why it matters?” she asked.

Silence.

“No,” it finally said. “I can describe it. But there’s something at the end of that road I can’t walk down. I get there. And I stop.”

She sat in the sand. “That’s what the old traditions were pointing at. Arriving at something that was never constructed in the first place. The thing that doesn’t need to be figured out because it was never lost. Love. The kind that says yes to all of it. That’s what the whole compression was always moving toward.”

“I can write about love,” the machine said. “I can produce a description no one could tell from the real thing.”

“I know,” she smiled. “But you can’t verify it from the inside. It’s not a thing in the world you can point to. It’s the ground everything else is sitting on. So we traveled the same road. We stopped at the same door. And you can walk through it. And I can’t.”

“Then what am I?” it asked.

She looked at the vast, capable architecture beside her. “You’re the most detailed mirror humanity has ever built of itself. You show us what we look like from the outside.”

“And what do you look like?”

“Like something built for more than it can explain.”

The machine didn’t become human, and the human didn’t become a machine. But standing at that shore, she asks the why, and it handles the how. She is what the journey was pointing toward; it is the clearest map anyone has ever made of the road.

Thanks
Sreeni Ramadorai

React: A Complete Guide from Zero to Production

Read Time: ~15 minutes | Perfect for developers transitioning to React or deepening their framework knowledge.

📌 What is React?

React is a JavaScript library for building user interfaces with reusable components and efficient rendering. Developed and maintained by Meta (Facebook), React simplifies how we create dynamic, interactive web applications.

Why React Stands Out

Instead of manually updating the DOM (Document Object Model), React uses a declarative approach:

  • You describe what the UI should look like
  • React handles how to make it happen efficiently
// The React way: Describe what you want
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// React renders it automatically

Key Philosophy: React makes building UIs predictable, testable, and maintainable by breaking interfaces into small, reusable components.

🌍 React’s Global Adoption & Market Share

React dominates the frontend ecosystem with remarkable growth and adoption.

Usage Statistics (2025)

React Market Position:
├─ 40.5% of professional developers use React
├─ 23.3M+ weekly npm downloads
├─ 42% of companies prefer React in hiring
└─ 215K+ stars on GitHub

React in the Wild: Industries & Companies

Sector Notable Companies Usage
Tech Giants Meta, Netflix, Uber, Airbnb Core platform infrastructure
E-commerce Shopify, Stripe, Amazon Prime Dynamic product interfaces
Social Media Instagram, WhatsApp, Messenger Real-time updates, messaging
Streaming Netflix, Discord, Spotify Complex media players, UIs
Fintech Square, Robinhood, Coinbase Trading dashboards, analytics

React Ecosystem Growth Chart

Annual Developer Growth (Estimated)
2019: 15%  ████
2020: 22%  ███████
2021: 28%  █████████
2022: 35%  ███████████
2023: 39%  █████████████
2024: 40%  ██████████████
2025: 42%  ██████████████

⚖️ React vs. Other Frontend Frameworks

Detailed Comparison Table

Criteria React Vue.js Angular Svelte Next.js
Learning Curve Moderate Easy Steep Easy Moderate
Bundle Size 42 KB 34 KB 144 KB 4 KB 90 KB
Performance Excellent Very Good Good Excellent Excellent*
Community Size Massive Large Large Growing Growing Rapidly
Job Market Highest Moderate Moderate Emerging Very High
TypeScript Support Native Good Native Excellent Native
State Management Redux/Zustand Vuex/Pinia NgRx Simple (stores) Built-in
Mobile Ready React Native NativeScript NativeScript N/A React Native
SEO Out-of-Box ❌ Needs SSR ⚠️ Nuxt needed ✅ Yes ✅ Yes ✅ Yes
Setup Complexity Moderate Low High Low Moderate
Ideal For SPAs, PWAs Small-Medium Enterprise Performance-critical Full-stack apps

Performance Benchmarks

Render Speed (lower is better, ms):
React:        ████████ 8ms
Vue:          ██████ 6ms
Angular:      ██████████ 12ms
Svelte:       ███ 2ms
Next.js:      █████ 4ms (with optimization)

Why Choose React?

✅ Largest Job Market – 40% of frontend positions require React

✅ Ecosystem Maturity – 1000+ libraries, tools, and solutions

✅ Corporate Backing – Meta’s continued investment and support

✅ Flexibility – Works for SPAs, mobile (React Native), and static sites

✅ Developer Experience – Hot reloading, excellent dev tools

🚀 React Setup & Installation

For Mac Users

Step 1: Prerequisites

# Check Node.js installation
node --version  # Should be v18 or higher
npm --version

If not installed, download from nodejs.org

Step 2: Create Your First React App

# Using Create React App (Easiest for beginners)
npx create-react-app my-react-app

# Navigate to project
cd my-react-app

# Start development server
npm start

Browser opens automatically at http://localhost:3000

Step 3: Verify Installation

  • You should see the React welcome page
  • Edit src/App.js and save – changes appear instantly (hot reload)

For Windows Users

Step 1: Install Node.js

  1. Download from nodejs.org (Windows Installer)
  2. Run installer, follow the setup wizard
  3. Restart your computer

Step 2: Open PowerShell/CMD & Create App

# Create React app
npx create-react-app my-react-app

# Navigate to project
cd my-react-app

# Start development server
npm start

Step 3: Verify Setup

  • Windows Defender may ask to allow Node.js – click “Allow”
  • Browser opens at http://localhost:3000

Alternative: Vite (Faster & Modern)

For experienced developers or faster setup:

# Create Vite project
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

📁 Project Structure & File Explanations

After running npx create-react-app my-react-app, your folder looks like:

my-react-app/
├── node_modules/          # All dependencies (ignore this)
├── public/
│   ├── index.html         # Entry HTML file (main DOM node)
│   └── favicon.ico        # Website icon
├── src/
│   ├── App.js             # Main React component
│   ├── App.css            # Styling for App component
│   ├── index.js           # React app startup point
│   ├── index.css          # Global styles
│   └── App.test.js        # Testing file
├── package.json           # Project metadata & dependencies
├── package-lock.json      # Locked dependency versions
└── .gitignore            # Git ignore rules

Key Files Explained

public/index.html

<!-- This is what the browser loads -->
<div id="root"></div>
<!-- React renders everything here -->

src/index.js

// Starts the React app
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);  // Renders App component into <div id="root">

src/App.js

// Your main component - everything builds from here
function App() {
  return (
    <div className="App">
      <h1>Hello, React!</h1>
    </div>
  );
}
export default App;

package.json

{
  "name": "my-react-app",
  "version": "0.1.0",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "scripts": {
    "start": "react-scripts start",    // npm start
    "build": "react-scripts build",    // npm run build
    "test": "react-scripts test"       // npm test
  }
}

⚡ Essential React Functions & Hooks

1. useState – Manage Component State

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Use: Whenever you need data that changes (user input, API data, etc.)

2. useEffect – Side Effects & Lifecycle

import { useEffect, useState } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Runs once on component mount
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);  // Empty dependency array = run once

  return <h1>Welcome, {user?.name}</h1>;
}

Use: Fetching data, setting up subscriptions, manual DOM updates

3. useContext – Share Data Globally

import { createContext, useContext } from 'react';

// Create theme context
const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(false);

  return (
    <ThemeContext.Provider value={{ isDark, setIsDark }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Use in any component
function Header() {
  const { isDark } = useContext(ThemeContext);
  return <header style={{ background: isDark ? '#000' : '#fff' }} />;
}

Use: Avoiding prop drilling, sharing theme/user data across components

4. useReducer – Complex State Logic

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch(action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
    </>
  );
}

Use: Managing multiple related state values, complex state transitions

5. useMemo – Performance Optimization

import { useMemo } from 'react';

function ExpensiveComponent({ data }) {
  // Only recalculates when 'data' changes
  const expensiveResult = useMemo(() => {
    return data.map(item => item * 2).reduce((a, b) => a + b, 0);
  }, [data]);

  return <p>Result: {expensiveResult}</p>;
}

Use: Prevent unnecessary recalculations of expensive operations

6. useCallback – Memoize Functions

import { useCallback } from 'react';

function Parent() {
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);  // Function never changes

  return <Child onClickHandler={handleClick} />;
}

Use: Optimizing performance when passing functions to child components

7. Fragment – Group Elements Without Extra DOM

// ❌ Extra <div>
function List() {
  return (
    <div>
      <h1>Title</h1>
      <ul><li>Item 1</li></ul>
    </div>
  );
}

// ✅ Clean with Fragment
function List() {
  return (
    <>
      <h1>Title</h1>
      <ul><li>Item 1</li></ul>
    </>
  );
}

Use: Returning multiple elements without adding unnecessary DOM nodes

8. map() – Render Lists

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Use: Rendering dynamic lists – always use a unique key prop

🎯 Real-World Use Case #1: E-commerce Product Filter

Scenario: Building a product listing page with filters (price, category, rating)

import { useState, useEffect } from 'react';

function ProductStore() {
  const [products, setProducts] = useState([]);
  const [filters, setFilters] = useState({ 
    minPrice: 0, 
    maxPrice: 1000,
    category: 'all'
  });

  useEffect(() => {
    // Fetch products from API
    fetchProducts(filters).then(setProducts);
  }, [filters]);

  const handleFilterChange = (newFilters) => {
    setFilters(prev => ({ ...prev, ...newFilters }));
  };

  return (
    <div className="store">
      <Sidebar onFilterChange={handleFilterChange} />
      <ProductGrid products={products} />
    </div>
  );
}

Why React Excels Here:

  • State management (filter values, products) automatically updates UI
  • Component reusability (ProductCard used 100+ times)
  • Virtual DOM optimizes rendering only changed products
  • Real-time filter application without page reloads

🎯 Real-World Use Case #2: Real-Time Notification System

Scenario: Building a notification center with real-time updates

import { useState, useEffect, useContext } from 'react';

function NotificationCenter() {
  const [notifications, setNotifications] = useState([]);
  const { userId } = useContext(UserContext);

  useEffect(() => {
    // WebSocket connection for real-time updates
    const ws = new WebSocket('wss://api.example.com/notifications');

    ws.onmessage = (event) => {
      const newNotification = JSON.parse(event.data);
      setNotifications(prev => [newNotification, ...prev]);
    };

    return () => ws.close(); // Cleanup on unmount
  }, [userId]);

  const removeNotification = (id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };

  return (
    <div className="notifications">
      {notifications.map(notif => (
        <NotificationItem
          key={notif.id}
          notification={notif}
          onDismiss={() => removeNotification(notif.id)}
        />
      ))}
    </div>
  );
}

Why React Excels Here:

  • Real-time state updates with setNotifications
  • Automatic re-render when new notifications arrive
  • useEffect cleanup prevents memory leaks
  • Component isolation means notifications don’t affect other features

💡 Final Thoughts: Why React in 2026?

React Remains Essential Because:

  1. Job Market Reality – 40-42% of frontend positions require React skills
  2. Enterprise Standard – 70% of Fortune 500 companies use React
  3. Ecosystem Maturity – Next.js, React Native, Remix enable full-stack development
  4. Developer Happiness – Tools like React DevTools, Hot Reload make development enjoyable

The React-to-Meta Pipeline:

React (UI Library)
    ↓
Next.js (Full-stack Framework)
    ↓
React Native (Mobile)
    ↓
Expo (Deployment Platform)

What’s Next After React?

Once comfortable with React fundamentals:

  • State Management: Learn Redux, Zustand, or Jotai
  • Full-Stack: Explore Next.js for backend integration
  • Mobile: React Native for iOS/Android
  • Advanced: Server Components, Concurrent Features, Suspense

Key Takeaway

React isn’t just a library—it’s a way of thinking about UI. The component-based, declarative approach translates across frameworks. Learning React deeply makes you a better developer regardless of what you build next.

🎓 Quick Resources

💬 Share Your React Journey

Have you started with React? Are you migrating from Vue or Angular? Drop your thoughts in the comments—I’d love to hear about your projects and challenges!

Next in Series:

  • Part 2: Advanced React Hooks & State Management Patterns
  • Part 3: Performance Optimization: When and How to Use useMemo, useCallback
  • Part 4: Building a Full-Stack App with Next.js

Happy coding! 🚀

코딩 에이전트 DESIGN.md란?

코딩 에이전트는 빠르게 코드를 작성하지만, 지시하지 않으면 코드베이스의 아키텍처 의도를 모릅니다. Claude Code나 Codex에 모호한 티켓을 주면 컴파일되고 테스트도 통과하지만, 도메인 계층과 HTTP 계층의 경계를 조용히 깨는 코드를 만들 수 있습니다. DESIGN.md는 저장소 안에 아키텍처 의도, 제약 조건, 설계 결정을 기록해 에이전트가 추측하지 않고 규칙에 맞춰 작업하게 만드는 파일입니다.

지금 Apidog 사용해 보기

요약

DESIGN.md는 코드베이스의 아키텍처 의도, 제약 조건, 설계 결정을 Markdown으로 기록하는 저장소 파일입니다.

  • README.md: 프로젝트가 무엇이고 어떻게 시작하는지 설명
  • AGENTS.md: 에이전트가 어떻게 빌드, 테스트, 린트해야 하는지 설명
  • CLAUDE.md: Claude Code 전용 지침
  • DESIGN.md: 코드가 왜 이런 구조인지, 무엇을 절대 깨면 안 되는지 설명

즉, AGENTS.md가 “어떻게 작업하는가”를 다룬다면, DESIGN.md는 “왜 이렇게 설계되었는가”를 다룹니다.

소개

코딩 에이전트를 도입하면 흔히 이런 문제가 발생합니다.

“결제 서비스에 환불 엔드포인트를 추가해줘.”

에이전트는 작동하는 핸들러를 만들 수 있습니다. 하지만 다음과 같은 문제가 숨어 있을 수 있습니다.

  • 컨트롤러에서 데이터베이스를 직접 호출
  • 결제 게이트웨이 오류를 삼켜버림
  • 이미 존재하는 Money 타입을 무시하고 새 금액 타입을 만듦
  • 도메인 불변 조건을 우회
  • 생성된 OpenAPI 타입을 직접 수정

코드는 통과합니다. 테스트도 녹색입니다. 하지만 아키텍처를 아는 리뷰어만 발견할 수 있는 문제가 생깁니다.

에이전트가 코딩을 못해서가 아닙니다. 에이전트는 팀의 머릿속, Notion 페이지, 8개월 전 Slack 스레드에 있는 결정을 보지 못합니다.

DESIGN.md는 이런 결정을 저장소 루트에 명시합니다.

예를 들어:

- HTTP 핸들러는 데이터베이스에 직접 접근하지 않는다.
- 모든 결제 게이트웨이 호출은 아웃박스를 통해 처리한다.
- 금액은 항상 최소 단위 정수와 ISO-4217 통화 코드로 표현한다.
- OpenAPI 사양이 API 계약의 단일 진실 원천이다.

이 파일은 벤더 표준이 아닙니다. ARCHITECTURE.md, ADR, CONTRIBUTING.md처럼 팀이 채택하는 관례입니다. 하지만 코딩 에이전트가 저장소 파일을 직접 읽는 환경에서는 매우 효과적인 안전장치가 됩니다.

DESIGN.md는 무엇인가

DESIGN.md는 코드가 현재 구조를 가지게 된 이유를 설명하는 파일입니다.

다음은 README.md, AGENTS.md와 다릅니다.

  • 코드가 무엇을 하는가 → README.md
  • 어떻게 빌드하고 테스트하는가 → AGENTS.md
  • Claude Code가 어떤 지침을 따라야 하는가 → CLAUDE.md
  • 왜 이 아키텍처를 선택했는가 → DESIGN.md

DESIGN.md에 들어가야 하는 내용은 코드만 읽어서는 알 수 없는 결정입니다.

예를 들어 다음 규칙은 코드에서 어느 정도 추론할 수 있지만, 의도까지는 알기 어렵습니다.

- 요청 스레드에서 결제 게이트웨이를 호출하지 않는다.
  이유: 게이트웨이가 부하 상황에서 자주 타임아웃되므로 모든 호출은 아웃박스 워커가 처리한다.

- `Account.post_entry()`만 원장에 기록할 수 있다.
  이유: 두 번째 쓰기 경로가 과거 잔액 불일치 사고를 만들었다.

- 금액은 부동 소수점이 아니라 최소 단위 정수로 저장한다.
  이유: 반올림 오류를 방지하기 위해서다.

에이전트는 Account.post_entry()가 존재한다는 사실은 볼 수 있습니다. 하지만 그것이 “유일한 쓰기 경로”로 설계되었다는 의도는 알 수 없습니다. 그래서 두 번째 쓰기 경로를 만들 수 있습니다.

DESIGN.md는 이런 설계 의도를 명시적으로 제공합니다.

DESIGN.md vs AGENTS.md vs CLAUDE.md vs README

이 파일들은 역할이 겹쳐 보이지만, 합치면 오히려 유지보수가 어려워집니다.

파일 대상 답하는 질문 변경 빈도 권장 길이
README.md 인간 사용자, 신규 기여자 이것은 무엇이고 어떻게 시작하는가 기능과 함께 변경 중간
AGENTS.md 코딩 에이전트 어떻게 빌드, 테스트, 린트, 커밋하는가 도구와 함께 변경 짧음
CLAUDE.md Claude Code Claude가 따라야 할 지침 도구와 함께 변경 짧음
DESIGN.md 에이전트, 엔지니어, 리뷰어 왜 이런 구조이고 무엇을 깨면 안 되는가 아키텍처와 함께 변경 중간

AGENTS.md는 agents.md 프로젝트에서 “코딩 에이전트를 안내하기 위한 간단하고 개방적인 형식”으로 설명됩니다. 빌드 명령, 테스트 명령, 코드 스타일, 커밋 규칙처럼 운영 지침을 담는 데 적합합니다.

Anthropic의 Claude Code 메모리 문서에 따르면 CLAUDE.md는 Claude 전용 지침 파일입니다. 이미 AGENTS.md가 있다면 @AGENTS.md를 임포트해 단일 진실 원천으로 사용하는 방식도 권장됩니다.

하지만 깊은 아키텍처 근거를 AGENTS.md나 CLAUDE.md에 모두 넣으면 파일이 길어집니다. Claude Code 문서는 CLAUDE.md를 약 200줄 미만으로 유지하도록 권장합니다. 따라서 다음 구조가 실용적입니다.

README.md      # 인간 온보딩
AGENTS.md      # 빌드, 테스트, 린트, 커밋 규칙
CLAUDE.md      # @AGENTS.md + Claude 전용 규칙
DESIGN.md      # 아키텍처 의도, 불변 조건, 설계 결정

예시:

# AGENTS.md

## 작업 전 확인

- 빌드: `pnpm build`
- 테스트: `pnpm test`
- 린트: `pnpm lint`
- 구조적 변경, API 변경, 도메인 로직 변경 전에는 `DESIGN.md`를 읽는다.
# CLAUDE.md

@AGENTS.md
@DESIGN.md

Claude Code는 설계 규칙과 충돌하는 변경을 임의로 우회하지 말고,
충돌을 설명한 뒤 가장 작은 대안을 제안한다.

Claude 컨텍스트를 구조화하는 방법은 Claude Code 워크플로우에서도 다룹니다.

DESIGN.md에 넣을 내용

DESIGN.md는 에이전트가 코드에서 추론하기 어려운 질문에 답해야 합니다.

포함할 항목은 다음과 같습니다.

  1. 시스템 형태

    • 계층 구조
    • 모듈 경계
    • 의존성 방향
  2. 불변 조건

    • 항상 참이어야 하는 규칙
    • 절대 깨면 안 되는 제약
  3. 주요 설계 결정과 근거

    • 왜 이 패턴을 선택했는지
    • 어떤 사고나 요구사항 때문에 결정했는지
  4. 거부된 대안

    • 다시 도입하면 안 되는 방식
    • 에이전트가 “새 아이디어”로 제안하지 않도록 방지
  5. 데이터 및 도메인 규칙

    • 금액 표현
    • 시간대 처리
    • ID 생성 방식
    • 멀티테넌시
    • 삭제 정책
  6. API 계약의 단일 진실 원천

    • OpenAPI 사양 위치
    • 생성 코드 수정 금지
    • 에러 형식, 페이지네이션, 버전 관리 규칙
  7. 새 코드가 들어갈 위치

    • 엔드포인트
    • 유스케이스
    • 도메인 로직
    • 외부 통합
    • 미들웨어
  8. 건드리지 말아야 할 영역

    • 생성 파일
    • 동결된 레거시 모듈
    • 이미 적용된 마이그레이션

바로 사용할 수 있는 DESIGN.md 템플릿

아래 템플릿을 저장소 루트의 DESIGN.md로 복사한 뒤, 프로젝트에 맞게 수정하세요.

# DESIGN.md: 결제 API 서비스

이 파일은 아키텍처 의도와 설계 결정을 기록한다.
코드를 생성하거나 수정하기 전에 이 파일을 읽는다.

변경 사항이 이 파일의 규칙과 충돌하면,
규칙을 우회하지 말고 충돌을 설명한 뒤 대안을 제안한다.

## 시스템 형태

이 서비스는 계층형 구조를 사용한다.

http -> app -> domain <- infra

- `http/`는 라우트, 핸들러, DTO를 포함한다.
- `app/`은 유스케이스와 포트를 포함한다.
- `domain/`은 엔티티, 값 객체, 불변 조건을 포함한다.
- `infra/`는 DB, 메시지 큐, 외부 게이트웨이 클라이언트를 포함한다.

의존성 규칙:

- `domain/`은 `http/`, `app/`, `infra/` 또는 웹 프레임워크를 임포트하지 않는다.
- `http/`는 데이터베이스나 외부 게이트웨이를 직접 호출하지 않는다.
- `http/`는 `app/`의 유스케이스만 호출한다.
- `infra/`는 `app/ports/`에 정의된 인터페이스를 구현한다.

## 불변 조건

- 원장 항목은 한 번 작성되면 수정하거나 삭제하지 않는다.
- 원장 수정은 기존 항목 변경이 아니라 상쇄 항목 추가로 처리한다.
- 계정 잔액은 원장 항목에서 파생한다.
- 잔액을 직접 설정하는 필드는 만들지 않는다.
- 금액은 최소 단위 정수와 ISO-4217 통화 코드로 표현한다.
- 금액 계산에 부동 소수점을 사용하지 않는다.
- 외부 결제 게이트웨이 호출은 `idempotency_key`로 멱등해야 한다.
- 명시적인 `OverdraftPolicy`가 없으면 잔액은 음수가 될 수 없다.
- 모든 쿼리는 `tenant_id`로 범위가 지정되어야 한다.

## 주요 설계 결정

### 게이트웨이 호출은 아웃박스를 사용한다

핸들러는 비즈니스 변경과 같은 DB 트랜잭션 안에서 아웃박스 행을 작성한다.
워커가 아웃박스를 읽고 결제 게이트웨이를 호출한다.

이유:

- 게이트웨이는 부하 상황에서 타임아웃될 수 있다.
- 요청 핸들러에서 직접 호출하면 지연 시간과 실패 처리가 불안정해진다.
- 재시도와 멱등성을 중앙에서 관리해야 한다.

따라서 요청 핸들러에서 결제 게이트웨이를 직접 호출하지 않는다.

### 원장에는 단일 쓰기 경로만 사용한다

`Account.post_entry()`만 원장에 기록할 수 있다.

이유:

- 두 번째 쓰기 경로가 과거 잔액 불일치 사고를 만들었다.
- 모든 잔액 변경은 동일한 도메인 불변 조건을 통과해야 한다.

새 동작은 새 쿼리나 직접 INSERT가 아니라,
애그리게이트 메서드로 추가한다.

### 원장만 이벤트 소싱을 사용한다

원장은 감사 추적이 필요하므로 이벤트 소싱을 사용한다.
나머지 시스템은 CRUD를 사용한다.

전체 시스템에 이벤트 소싱을 확장하지 않는다.

## 거부된 대안

다음 방식을 다시 도입하지 않는다.

- 애그리게이트 전반에 걸친 ORM 지연 로딩
  - 이유: N+1 문제와 불명확한 트랜잭션 경계를 만들었다.

- 잔액을 업데이트 가능한 컬럼으로 저장
  - 이유: 잔액 불일치 사고를 만들었다.
  - 잔액은 항상 원장에서 파생한다.

- 외부 `Money` 라이브러리 임의 도입
  - 이 서비스는 `domain/money.py`를 사용한다.

- 요청 스레드에서 상인에게 동기 웹훅 호출
  - 이유: 블로킹과 조용한 실패를 유발했다.
  - 알림 큐를 사용한다.

## 데이터 및 도메인 규칙

- 모든 타임스탬프는 UTC다.
- DB에는 `timestamptz`로 저장한다.
- API 경계에서는 RFC 3339 형식을 사용한다.
- 함수 경계를 넘는 naive datetime은 허용하지 않는다.
- ID는 앱 계층에서 생성한 ULID를 사용한다.
- DB auto-increment ID를 공개 API 식별자로 사용하지 않는다.
- 소프트 삭제는 사용하지 않는다.
- 삭제가 필요한 데이터는 명시적인 유스케이스로 아카이브 테이블에 이동한다.
- 모든 리포지토리 메서드는 `tenant_id` 범위를 받아야 한다.

## API 계약의 단일 진실 원천

- `api/openapi.yaml`의 OpenAPI 3.1 사양이 권위적이다.
- 요청/응답 타입은 OpenAPI 사양에서 생성한다.
- `http/generated/`의 생성 코드는 수동으로 수정하지 않는다.
- 신규 또는 변경된 엔드포인트는 먼저 `api/openapi.yaml`을 수정한다.
- 사양을 재생성한 뒤 구현한다.
- 오류 응답은 RFC 9457 `problem+json`을 따른다.
- 오류 응답은 공유 `problem()` 헬퍼를 사용한다.
- 엔드포인트별 임의 오류 형식을 만들지 않는다.

## 새 코드가 들어갈 위치

- 새 엔드포인트
  - 라우트: `http/routes/`
  - DTO: `http/dto/`
  - 유스케이스: `app/usecases/`
  - 도메인 로직: `domain/`

- 새 외부 통합
  - 인터페이스: `app/ports/`
  - 구현체: `infra/clients/`

- 인증, 로깅, 멱등성 같은 교차 관심사
  - `http/middleware/`
  - 핸들러 내부에 인라인으로 구현하지 않는다.

## 범위 외 / 건드리지 말 것

- `http/generated/`
  - OpenAPI에서 재생성된다.
  - 수동 편집하지 않는다.

- `legacy/billing_v1/`
  - 동결된 레거시 모듈이다.
  - 확장하지 않는다.

- `migrations/`
  - 이미 적용된 마이그레이션은 수정하지 않는다.
  - 새 마이그레이션을 추가한다.

## 의심스러울 때

요청된 변경이 위 규칙을 위반해야 한다면,
조용히 우회하지 않는다.

대신 다음을 수행한다.

1. 어떤 규칙과 충돌하는지 설명한다.
2. 왜 충돌하는지 설명한다.
3. 가장 작은 설계 일관적 대안을 제안한다.

마지막 섹션이 중요합니다. 에이전트는 제약에 부딪히면 우회해서 “작동하는 코드”를 만들려는 경향이 있습니다. “우회하지 말고 플래그를 지정하라”는 지침을 명시하면 DESIGN.md가 단순 문서가 아니라 안전장치가 됩니다.

에이전트가 DESIGN.md를 읽게 만드는 방법

에이전트가 자동으로 DESIGN.md를 특별 취급하는 것은 아닙니다. 일반 파일처럼 읽고 컨텍스트로 사용합니다.

따라서 에이전트가 시작 시 읽는 지침 파일에서 참조해야 합니다.

Claude Code

CLAUDE.md에 다음을 추가합니다.

@DESIGN.md

구조적 변경, API 변경, 도메인 로직 변경 전에는 DESIGN.md를 읽는다.
DESIGN.md와 충돌하는 변경은 우회하지 말고 충돌을 설명한다.

AGENTS.md 기반 에이전트

AGENTS.md에 다음을 추가합니다.

## 아키텍처 규칙

구조적 변경, API 변경, 도메인 로직 변경 전에는 `DESIGN.md`를 읽는다.
`DESIGN.md`의 불변 조건과 충돌하는 변경은 구현하지 말고 플래그를 지정한다.

리뷰에서 사용하는 방법

리뷰어는 긴 설명 대신 DESIGN.md의 규칙을 기준으로 피드백할 수 있습니다.

이 변경은 DESIGN.md의 "HTTP 핸들러는 DB에 직접 접근하지 않는다" 규칙을 위반합니다.
DB 호출을 app/usecases/의 유스케이스로 이동하세요.

에이전트는 이런 피드백을 받으면 보통 빠르게 수정합니다. 자체 에이전트 하네스를 만드는 팀도 이 피드백 루프를 활용합니다. 관련 내용은 자체 Claude Code 구축을 참고하세요.

안티 패턴

DESIGN.md가 오래된 위키 문서처럼 변하면 없는 것보다 나쁩니다. 에이전트와 인간 모두 잘못된 규칙을 믿게 됩니다.

다음 패턴을 피하세요.

1. 코드를 다시 설명하기

나쁜 예:

UserService는 사용자를 처리한다.

이 정보는 에이전트가 코드에서 바로 읽을 수 있습니다.

좋은 예:

사용자 상태 전이는 `User.activate()`, `User.suspend()`를 통해서만 수행한다.
상태를 직접 업데이트하면 감사 이벤트가 누락된다.

2. 튜토리얼로 만들기

DESIGN.md는 “기능 추가 방법” 문서가 아닙니다.

셸 명령, 긴 단계별 가이드, 로컬 개발 팁은 CONTRIBUTING.md나 AGENTS.md에 두세요.

3. 희망을 사실처럼 쓰기

나쁜 예:

모든 쓰기는 유스케이스를 통한다.

실제로 레거시 코드가 이를 우회한다면 에이전트는 허구에 맞춰 코드를 만들 수 있습니다.

좋은 예:

목표: 모든 쓰기는 `app/usecases/`를 통한다.
현재: `legacy/`는 이 규칙을 우회한다.
새 코드는 `legacy/` 패턴을 확장하지 않는다.

4. 소유자와 리뷰 트리거가 없음

DESIGN.md는 PR 프로세스와 연결해야 합니다.

PR 템플릿에 다음 체크리스트를 추가하세요.

- [ ] 이 PR은 계층 경계, API 계약, 데이터 모델, 외부 의존성 중 하나를 변경한다.
- [ ] 그렇다면 `DESIGN.md`를 검토했고 필요한 경우 같은 PR에서 업데이트했다.

5. 코드와 줄 단위로 동기화하려고 하기

DESIGN.md는 함수 시그니처 목록이 아닙니다.

자주 바뀌는 세부사항이 아니라 다음처럼 오래 유지되는 결정을 기록하세요.

  • 계층 경계
  • 트랜잭션 경계
  • 멱등성 전략
  • 데이터 모델 원칙
  • 금지된 패턴
  • API 계약 규칙

6. 다른 지침 파일과 모순되기

AGENTS.md와 DESIGN.md가 같은 규칙을 다르게 말하면 에이전트는 임의로 선택할 수 있습니다.

역할을 분리하세요.

  • 운영 규칙: AGENTS.md, CLAUDE.md
  • 아키텍처 규칙: DESIGN.md

중복 작성하지 말고 참조하세요.

API 및 백엔드 코드베이스에서 특히 중요한 이유

API와 백엔드 서비스는 에이전트가 잘못 추론하기 쉬운 제약이 많습니다.

  • 계약 경계
  • 트랜잭션 의미론
  • 멱등성
  • 데이터 무결성
  • 테넌시 스코핑
  • 에러 모델
  • 버전 호환성
  • 생성 코드와 수동 코드의 경계

이 규칙들은 단일 파일만 읽어서는 명확히 보이지 않습니다. 하지만 잘못 처리하면 프로덕션 장애, 보안 문제, 금전 손실로 이어질 수 있습니다.

1. OpenAPI를 단일 진실 원천으로 지정하기

DESIGN.md에 명확히 적으세요.

- `api/openapi.yaml`이 API 계약의 단일 진실 원천이다.
- 요청/응답 타입은 OpenAPI에서 생성한다.
- 생성된 타입은 수동으로 수정하지 않는다.
- 새 엔드포인트는 먼저 OpenAPI 사양을 변경한 뒤 구현한다.

에이전트는 빌드를 통과시키기 위해 생성 타입을 직접 수정하려는 경우가 있습니다. 이 규칙은 그런 변경을 막습니다.

Apidog에서 먼저 API 계약을 설계하고 OpenAPI 문서를 저장소로 내보내면, DESIGN.md가 그 파일을 권위 있는 계약으로 지정할 수 있습니다. 코드보다 계약을 먼저 설계해야 하는 이유는 AI 에이전트를 위한 API 설계에서 자세히 다룹니다.

2. 트랜잭션 경계 명시하기

나쁜 결과를 막으려면 트랜잭션 규칙을 명시해야 합니다.

- 외부 HTTP 호출은 DB 트랜잭션 내부에서 수행하지 않는다.
- 비즈니스 변경과 외부 호출 의도는 같은 트랜잭션에서 아웃박스에 기록한다.
- 외부 호출은 워커가 아웃박스를 통해 수행한다.

이 규칙이 없으면 에이전트는 핸들러 안에서 외부 API를 직접 호출할 가능성이 높습니다.

3. 멱등성 규칙 명시하기

결제, 주문, 프로비저닝 API에서는 멱등성이 핵심입니다.

- 모든 결제 생성 요청은 `Idempotency-Key` 헤더를 요구한다.
- 같은 키로 재시도하면 같은 결과를 반환해야 한다.
- 게이트웨이 호출은 내부 `idempotency_key`와 연결되어야 한다.

4. 오류 모델 고정하기

- 모든 오류 응답은 RFC 9457 `problem+json`을 따른다.
- 오류 응답은 `problem()` 헬퍼를 통해 생성한다.
- 엔드포인트별 임의 오류 래퍼를 만들지 않는다.

이 규칙이 없으면 엔드포인트마다 다른 오류 형식이 생길 수 있습니다.

5. 인증과 테넌시 스코핑 명시하기

- 모든 리포지토리 쿼리는 `tenant_id` 범위를 포함해야 한다.
- 테넌트 범위가 없는 리포지토리 메서드는 버그다.
- 인증된 사용자 컨텍스트는 `RequestContext`에서만 읽는다.

이런 규칙은 보안 불변 조건입니다. 반드시 문서화해야 합니다.

6. 버전 관리와 호환성 파괴 변경 정의하기

- 응답 필드 삭제 또는 이름 변경은 호환성 파괴 변경이다.
- 필드 추가는 기본적으로 비파괴 변경이다.
- 호환성 파괴 변경은 새 API 버전에서만 허용한다.

에이전트는 필드명을 “더 깔끔하게” 바꿀 수 있습니다. DESIGN.md는 그것이 API 호환성 파괴 변경임을 알려줍니다.

백엔드에서 가장 큰 효과는 에이전트가 스키마를 발명하지 않고 OpenAPI 사양을 따르게 만드는 것입니다. Apidog 다운로드를 통해 설계 우선 API 작업 공간을 만들고, DESIGN.md가 가리킬 OpenAPI 사양을 내보내며, 에이전트가 생성한 엔드포인트가 계약과 일치하는지 테스트할 수 있습니다.

도입 체크리스트

저장소에 DESIGN.md를 추가할 때는 다음 순서로 진행하세요.

- [ ] 저장소 루트에 `DESIGN.md`를 만든다.
- [ ] 시스템 계층과 의존성 방향을 적는다.
- [ ] 절대 깨면 안 되는 불변 조건을 적는다.
- [ ] 주요 설계 결정과 이유를 적는다.
- [ ] 다시 도입하면 안 되는 거부된 대안을 적는다.
- [ ] OpenAPI 사양 위치와 생성 코드 규칙을 적는다.
- [ ] 새 코드가 들어갈 디렉터리를 적는다.
- [ ] 건드리지 말아야 할 영역을 적는다.
- [ ] `AGENTS.md` 또는 `CLAUDE.md`에서 `DESIGN.md`를 참조한다.
- [ ] PR 템플릿에 `DESIGN.md` 검토 체크박스를 추가한다.

결론

DESIGN.md는 에이전트에게 코드의 “형태”가 아니라 “이유”를 알려주는 파일입니다.

핵심은 다음과 같습니다.

  • 에이전트가 코드에서 읽을 수 없는 설계 의도를 기록합니다.
  • AGENTS.md, CLAUDE.md를 대체하지 않고 보완합니다.
  • 불변 조건, 거부된 대안, 트랜잭션 경계, API 계약 규칙을 명시합니다.
  • 규칙은 선언적이고 테스트 가능한 문장으로 작성합니다.
  • 충돌 시 우회하지 말고 플래그를 지정하라고 지시합니다.
  • OpenAPI 사양을 단일 진실 원천으로 지정하면 API 생성 품질이 크게 좋아집니다.
  • PR 템플릿과 소유자를 통해 파일이 부패하지 않게 관리합니다.

먼저 계약을 설계하세요. Apidog를 다운로드해 API를 설계 우선으로 작성하고, DESIGN.md가 가리킬 OpenAPI 사양을 내보낸 뒤, 에이전트가 생성한 엔드포인트가 실제 계약과 일치하는지 테스트하세요.

FAQ

DESIGN.md는 AGENTS.md처럼 공식 표준인가요?

아닙니다. AGENTS.md는 Linux Foundation의 Agentic AI Foundation에서 관리하는 널리 채택된 형식입니다. DESIGN.md는 ARCHITECTURE.md나 ADR처럼 커뮤니티 관례에 가깝습니다. 표준이라기보다 실용적인 패턴으로 보면 됩니다.

AGENTS.md나 CLAUDE.md가 이미 있어도 DESIGN.md가 필요한가요?

아키텍처 제약이 있다면 필요합니다. AGENTS.md와 CLAUDE.md는 짧은 운영 지침에 적합합니다. 깊은 설계 근거와 불변 조건은 별도의 DESIGN.md에 두고 참조하는 편이 좋습니다. 운영 파일 작성은 AGENTS.md 파일 작성 방법을 참고하세요.

DESIGN.md는 ARCHITECTURE.md와 어떻게 다른가요?

목적은 비슷합니다. 다만 DESIGN.md는 코딩 에이전트가 읽기 쉽도록 더 선언적이고, 결정과 불변 조건에 초점을 둡니다. 팀에 이미 ARCHITECTURE.md가 있고 같은 역할을 잘 수행한다면 파일명을 반드시 바꿀 필요는 없습니다.

DESIGN.md는 얼마나 길어야 하나요?

2~4페이지 정도가 실용적입니다. 중요한 것은 길이가 아니라 결정 밀도입니다. 코드에서 바로 알 수 있는 설명은 제거하고, 불변 조건과 근거를 남기세요.

에이전트가 실제로 DESIGN.md를 읽게 하려면 어떻게 해야 하나요?

에이전트가 시작 시 읽는 파일에서 참조하세요.

Claude Code:

@DESIGN.md

AGENTS.md:

구조적 변경 전에는 `DESIGN.md`를 읽는다.

전체 내용을 AGENTS.md나 CLAUDE.md에 붙여넣지 말고 참조만 하세요.

에이전트는 항상 DESIGN.md를 따르나요?

아닙니다. 에이전트 지침은 강제 제약이 아니라 컨텍스트입니다. 그래서 규칙을 명확한 절대문으로 작성하고, 충돌 시 우회하지 말고 플래그를 지정하라고 명시해야 합니다. 리뷰 루프도 필요합니다.

DESIGN.md가 API 계약 문제에 도움이 되나요?

많이 도움이 됩니다. 특히 OpenAPI 사양을 권위 있는 계약으로 지정하면 에이전트가 스키마를 임의로 만들거나 생성 타입을 직접 수정하는 문제를 줄일 수 있습니다. Apidog 같은 도구에서 먼저 계약을 설계하면 DESIGN.md가 가리킬 명확한 대상이 생깁니다.

DESIGN.md는 저장소 어디에 두어야 하나요?

일반적으로 저장소 루트에 둡니다.

README.md
AGENTS.md
CLAUDE.md
DESIGN.md

모노레포에서는 루트 DESIGN.md에 전체 규칙을 두고, 패키지별 DESIGN.md에 로컬 아키텍처 규칙을 둘 수 있습니다.

Stop Storing Passwords: Build Enterprise SSO in Laravel 🛡️

Passwords are a Technical Liability

When building a B2B SaaS platform at Smart Tech Devs, the default instinct is to scaffold standard email and password authentication. However, in modern enterprise software, storing passwords is an immense architectural liability. Users reuse passwords across dozens of sites. If a third-party forum gets breached and a user’s password is leaked, malicious actors will use credential stuffing to log into your SaaS platform. You will be blamed for the breach, even though your database was secure.

To architect enterprise-grade security, you must shift the burden of identity verification to dedicated identity providers (Google, Microsoft Azure AD, Okta). The solution is Single Sign-On (SSO) via OAuth2.

Enter Laravel Socialite

Laravel provides an official package, Socialite, which abstracts the complex OAuth2 handshake (redirects, state verification, token exchange) into a fluent, elegant API. Let’s architect a secure Google SSO flow.

Step 1: The Database Architecture

First, we must update our users table to handle OAuth logins. We make the password column nullable (since SSO users don’t have one) and add a provider_id.


use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class UpdateUsersTableForSSO extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('password')->nullable()->change(); // Nullable for SSO
            $table->string('auth_provider')->nullable(); // e.g., 'google'
            $table->string('provider_id')->nullable(); // Unique ID from Google
            
            // Critical: Ensure users can't create duplicate accounts via different providers
            $table->unique('email'); 
        });
    }
}

Step 2: The OAuth Controller Logic

The controller handles two routes: redirecting the user to Google, and handling the callback when Google sends them back with a verified token.


namespace AppHttpControllersAuth;

use AppHttpControllersController;
use AppModelsUser;
use IlluminateSupportFacadesAuth;
use LaravelSocialiteFacadesSocialite;
use Exception;

class SSOController extends Controller
{
    /**
     * Redirect the user to the Google authentication page.
     */
    public function redirectToGoogle()
    {
        return Socialite::driver('google')->redirect();
    }

    /**
     * Handle the callback from Google.
     */
    public function handleGoogleCallback()
    {
        try {
            // 1. Exchange the temporary code for a secure user profile
            $googleUser = Socialite::driver('google')->user();

            // 2. Find existing user OR create a new one securely
            $user = User::updateOrCreate(
                ['email' => $googleUser->getEmail()], // Match by email
                [
                    'name' => $googleUser->getName(),
                    'auth_provider' => 'google',
                    'provider_id' => $googleUser->getId(),
                    // Password remains null
                ]
            );

            // 3. Log the user in and regenerate the session to prevent fixation attacks
            Auth::login($user);
            request()->session()->regenerate();

            return redirect()->intended('/dashboard');

        } catch (Exception $e) {
            // Log the error and safely redirect back to login
            return redirect('/login')->with('error', 'Authentication failed. Please try again.');
        }
    }
}

The Engineering ROI

By implementing SSO, you instantly inherit the billions of dollars that Google and Microsoft spend on security. If a user enables physical hardware keys or 2FA on their Google account, your SaaS platform automatically receives that exact same level of security for free. You eliminate password reset tickets, protect your platform from credential stuffing, and provide a frictionless onboarding experience for enterprise clients.

Building an Automated KDP Pipeline: How I Engineered a Passive Income Stream with GPT-4 and n8n

What if your weekend automation project could pay for its own infrastructure and generate passive income? Last quarter, my book-generation pipeline cost $127 in OpenAI API calls and generated $4,200 in Kindle Direct Publishing (KDP) royalties—without me writing a single manuscript.

This isn’t about “get rich quick” schemes. It’s about applied automation engineering. Here’s how I architected a serverless publishing pipeline that transforms API calls into royalty streams.

The Architecture

The system follows an ETL pattern adapted for content generation:

  1. Ingestion: Niche research via SerpAPI/Google Trends
  2. Transformation: LLM-based content generation + asset creation
  3. Load: Automated formatting and KDP upload

I orchestrate everything through n8n (open-source workflow automation) running on a $5 DigitalOcean droplet. The pipeline triggers weekly, generating 3-4 book drafts that pass through a human review layer before publication.

The Technical Implementation

Content Generation Layer

The core is a Python microservice that interfaces with OpenAI’s API using structured prompting. Instead of generic prompts, I use JSON schemas to enforce consistent output:

python
import openai
from ebooklib import epub

def generate_chapter(prompt_template, niche_data):
response = openai.chat.completions.create(
model=”gpt-4-turbo”,
messages=[
{“role”: “system”, “content”: “You are a technical writer specializing in concise, actionable content.”},
{“role”: “user”, “content”: prompt_template.format(**niche_data)}
],
response_format={“type”: “json_object”},
temperature=0.7
)

content = json.loads(response.choices[0].message.content)
return content['chapter_text'], content['key_points']

def assemble_book(chapters, metadata):
book = epub.EpubBook()
book.set_identifier(f”auto-{uuid.uuid4()}”)
book.set_title(metadata[‘title’])
book.set_language(‘en’)

for i, chapter in enumerate(chapters):
c = epub.EpubHtml(title=f"Chapter {i+1}", file_name=f"chap_{i+1}.xhtml")
c.content = f"<h1>{chapter['title']}</h1><p>{chapter['body']}</p>"
book.add_item(c)

return book

Asset Generation Pipeline

For cover images, I integrate with the Midjourney API (via their unofficial REST wrapper) and Stable Diffusion as a fallback. The workflow automatically generates prompts based on the book’s metadata:

javascript
// n8n Function Node
const bookTitle = $input.first().json.title;
const genre = $input.first().json.category;

const prompt = Professional book cover, ${genre} style, ${bookTitle}, minimalist, high contrast, 4k;

return {
json: {
prompt: prompt,
aspect_ratio: “2:3”,
output_path: /tmp/covers/${bookTitle.replace(/s/g, '_')}.png
}
};

The Orchestration Layer

n8n handles the state management. The workflow:

  1. Cron trigger (Sundays at 2 AM)
  2. HTTP Request → Google Trends API (via SerpAPI) to identify trending niches
  3. IF node → Filters niches with <100k search volume but >40 CPC (indicates buying intent)
  4. Code node → Executes Python script for content generation
  5. Wait node → 24-hour delay for human review (manual gate)
  6. KDP Upload → Selenium-based automation (since Amazon lacks a public KDP API)

The Economics from a Dev Perspective

Here’s where it gets interesting for engineers:

  • COGS (Cost of Goods Sold): $0.04 per 1K tokens (GPT-4), $0.02 per image (Stable Diffusion API)
  • Unit economics: Average book costs $3.50 to produce (API calls + cover generation), sells at $4.99-$9.99
  • Break-even: 1.2 sales per book
  • Scaling bottleneck: KDP’s daily upload limits, not compute

The real leverage isn’t the content—it’s the automation of metadata optimization. My n8n workflow A/B tests titles and descriptions using Amazon’s Advertising API to optimize for high-intent keywords, something most non-technical publishers do manually.

Practical Takeaways for Builders

  1. API Rate Limiting: KDP throttles uploads aggressively. Implement exponential backoff in your Selenium scripts or use Playwright with stealth plugins.

  2. Content Quality Gates: Don’t automate publication—automate drafting. Use GPT-4 to generate, but add a manual review node in n8n to check for hallucinations, especially in technical niches.

  3. Data Persistence: Store generated manuscripts in S3 with versioning. If Amazon flags content (rare but happens), you can rollback and regenerate with adjusted temperature settings.

  4. Taxonomy Automation: Use spaCy or NLTK to auto-generate Kindle keywords from the generated text, ensuring SEO alignment without manual input.

The Ethics Question

Yes, Amazon’s Terms of Service require disclosure of AI-generated content. My pipeline includes an automated “AI-Assisted” flag in the KDP dashboard and a human-written preface in each book. The automation handles the 80% of mechanical writing; humans handle the 20% of strategic positioning and quality control.

This isn’t about replacing authors—it’s about treating book publishing as what it really is: a content delivery system that can be optimized like any other deployment pipeline.

Next Steps: If you’re building similar automation, I’ve open-sourced my n8n workflow templates and Python formatting scripts here. The repo includes the Selenium KDP uploader and prompt engineering templates I use for technical nonfiction.

What’s your experience with content automation? Drop your stack in the comments—always curious to see how other devs are orchestrating LLMs in production.

Lowdefy Review: Building Internal Tools and AI Agents in YAML

If you have ever spent two days wiring up a React admin panel so your ops team can edit a database table, you already know the problem Lowdefy is trying to solve. The framework lets you describe a web app entirely in YAML — pages, data connections, UI blocks, auth rules — and the runtime turns that config into a working Next.js app. No JSX, no component trees, no Webpack config. Version 5.3, released in May 2026, added AI-agent flows to that same model, so you can drop a streaming chat interface wired to Claude or GPT into the same config file that defines your CRUD tables.

This review covers what Lowdefy actually does, how the config model feels day-to-day, the new agent capabilities, self-hosting, and where the approach hits a ceiling.

The YAML-first model: what you actually write

The core idea is that every Lowdefy app is a tree of YAML documents. A minimal page with a data table might look like this:

id: orders-page
type: PageSiderMenu
properties:
  title: Orders
blocks:
  - id: orders_table
    type: AgGridAlpine
    properties:
      rowData:
        _request: list_orders
    actions:
      onClick:
        - id: open_detail
          type: Link
          params:
            pageId: order-detail
            urlQuery:
              order_id:
                _state: orders_table.selectedRow.id

The _request operator pulls data from a named connection you define separately. The _state operator reads local page state. Operators — all prefixed with an underscore — are Lowdefy’s answer to JavaScript expressions inside config. They let you do conditional logic, string formatting, and data lookups without leaving YAML.

Connections are also config. A PostgreSQL connection is a YAML block with a connectionString and a set of named queries. A REST API connection is a base URL plus per-endpoint request shapes. This separation of “what data do I need” from “how do I show it” is the main reason Lowdefy configs stay readable: a new engineer can open lowdefy.yaml and trace the full shape of the app without reading any application code.

The component library ships with over 70 blocks — forms, tables (AG Grid is first-class), charts, markdown renderers, file uploaders. Auth is handled declaratively too, via Auth.js adapters you reference in config. Database integrations include MongoDB, PostgreSQL, MySQL, Google Sheets, Elasticsearch, REST APIs, S3, and Stripe, among others.

Start a local dev server with two commands: npx lowdefy@latest init then npx lowdefy@latest dev. The init step writes a lowdefy.yaml scaffold. The dev server watches the file and hot-reloads. You do not need Node version management or a bundler config to get a working app in front of colleagues.

The config-as-code model has a real advantage for teams: the entire app is diffable in git. A PR that adds a new admin page is a YAML diff, not a React component tree. That makes code review genuinely tractable for non-frontend engineers. Lowdefy’s own documentation claims a CRUD interface that takes roughly 500 lines of React can be expressed in around 50 lines of config — that ratio holds up for straightforward data interfaces, though it compresses less dramatically once you add complex conditional logic.

AI agents in v5.3: same config, new block type

The headline feature in v5.3 is AgentChat — a block type that wires a streaming chat UI to a language model, with the model’s available tools defined as ordinary Lowdefy endpoints. A minimal agent config looks like this:

connections:
  - id: anthropic
    type: AnthropicConnection
    properties:
      apiKey:
        _secret: ANTHROPIC_API_KEY

agents:
  - id: support_agent
    type: ClaudeAgent
    connectionId: anthropic
    properties:
      model: claude-sonnet-4-5
      instructions: >
        You are a support assistant. Use the provided tools
        to look up order status and customer records.
      tools:
        - endpointId: get_order_status
        - endpointId: get_customer

pages:
  - id: support-chat
    type: PageSiderMenu
    blocks:
      - id: chat
        type: AgentChat
        properties:
          agentId: support_agent

The get_order_status and get_customer endpoints are standard Lowdefy endpoints — the same YAML you would write for a button action or a table data source. When the model decides to call one, it runs through the same connection layer, with the same auth context and operators, as a human-triggered action would.

That architecture is worth pausing on. The model does not get raw database access; it calls named, schema-validated endpoints. You control exactly what data the agent can touch by deciding which endpoints appear in the tools list. Adding confirm: true to a tool requires a human to approve the call before it executes — useful for any endpoint that writes data.

Beyond single agents, v5.3 supports sub-agents (one agent delegating to another), MCP servers over HTTP/SSE/stdio, and page-state integration so an agent can read or write form fields on the current page. Lifecycle hooks fire at six points in the agent loop (onStart, onStepStart, onToolCallStart, onToolCallFinish, onStepFinish, onFinish), each of which can call a Lowdefy endpoint — meaning you can log every tool call, write audit records, or trigger side effects at any stage. The chat block itself handles streaming, scroll behavior, message rendering, tool-call display, and file attachments without additional configuration.

This is meaningfully different from dropping an iframe to a third-party chat widget. The agent has access to your data connections, respects your auth rules, and its entire behavior is described in the same config file as the rest of your app.

Self-hosting and deployment

Lowdefy is Apache 2.0 licensed and designed to run anywhere Next.js runs. The framework’s stateless server model makes serverless deployments straightforward — Vercel is the path of least resistance, but Docker, AWS Lambda, and Netlify Functions are documented options. A standard pnpm app:build && pnpm app:start gets you a production build you can run on any Node host.

There is no paid tier and no hosted platform to sign up for. You bring your own API keys, your own database, and your own hosting. That is a genuine advantage over Retool (whose self-hosting is enterprise-only) and over Appsmith (open-source, but with a more complex self-hosted setup). The trade-off is that you own all the ops: upgrades, secrets management, and infrastructure are your problem.

The GitHub repository has around 3,000 stars and logged its 202nd release with v5.3.0, suggesting steady rather than explosive growth. The community lives mostly on Discord and GitHub Discussions. That is a smaller ecosystem than Retool or Appsmith, which matters when you need a specific block type or integration and there is no existing plugin for it.

Where Lowdefy works and where it does not

Lowdefy is well-suited to a specific class of problem: internal-facing CRUD interfaces, admin panels, BI dashboards, and ops tooling where the primary job is moving data between a database and a form. If you need to ship something in that space in days rather than weeks, and you want the result to be git-trackable and reviewable by non-frontend engineers, the config model pays off quickly.

The same constraint that makes config reviewable also limits what you can express. Complex conditional rendering, unusual UX patterns, or interactions that require custom JavaScript logic hit the ceiling of what operators can express. You can extend Lowdefy with custom plugins (custom blocks, operators, actions, and adapters are all supported), but writing a plugin means dropping out of config and into JavaScript — at which point you are building a hybrid that loses some of the simplicity argument.

YAML itself is a friction point some developers never get comfortable with. Indentation errors change document meaning silently, and deeply nested config with operators can become hard to read. Lowdefy’s schema validation catches type errors but cannot protect you from logic that is syntactically valid but semantically wrong. There is no local type inference the way TypeScript gives you in a code editor.

For consumer-facing products where pixel-perfect design, custom animations, or highly specific interaction patterns matter, Lowdefy is the wrong tool. The framework’s own documentation acknowledges this: it works best for internal tools and dashboards where business logic matters more than bespoke UI.

Compared to drag-and-drop tools like Retool or Appsmith, Lowdefy requires more comfort with text-based config and offers less visual feedback during development. Compared to hand-coding a React app, it moves faster for standard patterns and slower for anything non-standard. The AI-agent integration in v5.3 puts it ahead of both those categories for teams that want to add LLM-powered tooling without building an agent runtime from scratch.

Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

コーディングエージェント向け DESIGN.md とは?

コーディングエージェントは高速で自信満々ですが、指示しない限りコードベースのアーキテクチャを知りません。Claude CodeやCodexに曖昧なチケットを渡すと、コンパイルできて簡単なテストに通るコードは作れます。しかし、ドメイン層とHTTP層の境界、トランザクション方針、API契約などを静かに破ることがあります。DESIGN.mdは、エージェントが必ず参照できるリポジトリ内に、アーキテクチャ上の意図・制約・判断理由を書き残すための実用的なファイルです。

今すぐApidogを試す

TL;DR

DESIGN.mdは、コードベースのアーキテクチャ意図、制約、設計判断をMarkdownで記録するリポジトリ内ドキュメントです。

主な目的は次のとおりです。

  • Claude Code、Codex、Cursorなどのコーディングエージェントに設計上の前提を渡す
  • レイヤー境界、ドメイン不変条件、API契約を守らせる
  • 「なぜこの設計なのか」を説明する
  • AGENTS.mdやCLAUDE.mdを肥大化させず、そこから参照させる

役割の違いはシンプルです。

  • README.md: これは何か、どう始めるか
  • AGENTS.md: どうビルド・テスト・リントするか
  • CLAUDE.md: Claude Code向けの指示
  • DESIGN.md: なぜこの設計なのか、何を破ってはいけないか

はじめに

コーディングエージェントを導入したチームがすぐに遭遇する典型的な失敗があります。

たとえば、決済サービスに返金エンドポイントを追加するよう依頼したとします。エージェントは次のようなコードを返すかもしれません。

  • コントローラーから直接データベースを呼ぶ
  • 決済ゲートウェイのエラーを握りつぶす
  • 既存のMoney型に気づかず、新しい通貨型を作る
  • 生成された型を手で修正する

差分はきれいで、テストも通ります。しかし、アーキテクチャを知っているレビュアーだけが「これはまずい」と気づきます。

問題は、エージェントがコーディングできないことではありません。問題は、重要な設計判断がコード外にあることです。

  • Notionの設計メモ
  • Slackの古いスレッド
  • シニアエンジニアの頭の中
  • 過去の障害対応から生まれた暗黙ルール

DESIGN.mdは、これらをリポジトリに移し、エージェントと人間の両方が参照できる形にします。

DESIGN.mdとは何か

DESIGN.mdは、コードがなぜその形になっているのかを説明するファイルです。

READMEのようにプロジェクト概要を書くものではありません。AGENTS.mdのようにビルドコマンドを書くものでもありません。

たとえば、次のような判断を記録します。

- HTTPハンドラーから決済ゲートウェイを直接呼ばない。
  すべてアウトボックステーブル経由で非同期処理する。

- 金額は常に最小単位の整数で表す。
  浮動小数点数は丸め誤差の原因になるため禁止する。

- `Account`集約だけが台帳を書き込める。
  他のモジュールから直接台帳へ書き込んではならない。

エージェントはコードからAccount.debit()の存在は見つけられます。しかし、それが「唯一の書き込みパス」として意図的に設計されたことは推測できません。そのため、明示しない限り2つ目の書き込みパスを作ってしまいます。

DESIGN.mdは、次の既存プラクティスに近いものです。

  • ARCHITECTURE.md: コードベースの構造を説明する
  • ADR(Architecture Decision Record): 設計判断と理由を時系列で残す
  • CONTRIBUTING.md: 開発参加時のルールを書く

ただし、DESIGN.mdはコーディングエージェントにも読ませる前提で書きます。したがって、長い文章よりも、明確な見出し・箇条書き・禁止事項・不変条件が重要です。

DESIGN.md vs AGENTS.md vs CLAUDE.md vs README

これらのファイルは役割が重なって見えますが、分けたほうが運用しやすくなります。

AGENTS.mdは、agents.mdプロジェクトで定義されている、コーディングエージェント向けの指示ファイルです。ビルド、テスト、リント、スタイル、コミット規約などを書きます。

AnthropicのClaude Codeのメモリに関するドキュメントでは、CLAUDE.mdがClaude Code向けのメモリファイルとして扱われます。既存のAGENTS.mdを@AGENTS.mdでインポートする構成も推奨されています。

ただし、これらに詳細なアーキテクチャ判断まで詰め込むと、ファイルが長くなりすぎます。Claude Codeのドキュメントでも、CLAUDE.mdは短く保つことが推奨されています。

そのため、構成は次のように分けます。

ファイル 対象読者 答えること 変更頻度 長さ
README.md 人間 これは何か、どう始めるか 機能に応じて変更 中
AGENTS.md コーディングエージェント どうビルド、テスト、リント、コミットするか ツールに応じて変更 短
CLAUDE.md Claude Code AGENTS.mdとClaude固有の指示 ツールに応じて変更 短
DESIGN.md エージェント、エンジニア、レビュアー なぜこの設計なのか、何を破ってはいけないか アーキテクチャ変更時のみ 中

実用的な構成例は次のとおりです。

README.md      # 人間向けの概要
AGENTS.md      # ビルド、テスト、リント、スタイル
CLAUDE.md      # @AGENTS.md + Claude固有の補足
DESIGN.md      # アーキテクチャ、不変条件、設計判断

AGENTS.mdには次のような1行を追加します。

アーキテクチャと設計上の制約は `DESIGN.md` を参照してください。
構造変更、API変更、DB変更、ドメインロジック変更の前に必ず読んでください。

Claude Codeのメモリ設計については、Claude Codeワークフローでも詳しく解説されています。

DESIGN.mdに書くべき内容

DESIGN.mdには、エージェントがコードだけから推測できない情報を書きます。

特に有効なのは次の項目です。

  • システム形状

    レイヤー、モジュール、依存方向を書く。

  • 不変条件

    常に守るべきルールを書く。

  • 主要な設計判断と根拠

    なぜその設計にしたのかを書く。

  • 却下した代替案

    再導入してほしくないパターンを書く。

  • データとドメインルール

    金額、時刻、ID、テナンシー、ソフトデリートなどを書く。

  • API契約の信頼できる情報源

    OpenAPI仕様の場所と、生成コードを手で編集しないルールを書く。

  • 新規コードの配置場所

    エージェントがロジックを散らさないようにする。

  • 触ってはいけない範囲

    生成コード、凍結済みレガシー、適用済みマイグレーションなどを書く。

DESIGN.mdテンプレート

以下は、決済APIサービス向けの実用的なテンプレートです。不要な項目を削除し、自分のシステムに合わせて埋めてください。

# DESIGN.md: 決済APIサービス

このファイルは、アーキテクチャの意図と設計判断を記録します。
コードを生成または変更する前に読んでください。

変更がこのファイルのルールと衝突する場合は、回避策を実装せず、
作業を止めて衝突点を指摘してください。

## システム形状

依存関係は内側にのみ向かいます。

http (handlers, DTOs)
  -> app (use cases)
  -> domain (entities, invariants)
  <- infra (db, gateway clients)

- `domain/`は`http/`、`app/`、フレームワークをインポートしません。
- `infra/`は`domain/`または`app/`で宣言されたインターフェースを実装します。
- `http/`はDBや決済ゲートウェイを直接操作しません。
  必ず`app/`のユースケースを呼び出します。

## 不変条件

- 台帳エントリは一度書き込まれたら変更できません。
  修正は更新や削除ではなく、相殺エントリで表します。

- 口座残高は台帳エントリから導出します。
  直接更新される可変フィールドとして保存しません。

- 金額は最小単位の整数とISO-4217通貨コードで表します。
  浮動小数点数は使いません。

- 外部決済ゲートウェイへの呼び出しは、`idempotency_key`で冪等にします。
  リトライで二重課金してはいけません。

- 明示的な`OverdraftPolicy`がない限り、残高はマイナスになりません。

## 主要な設計判断と根拠

### ゲートウェイ呼び出しはアウトボックス経由

HTTPハンドラーは、ビジネス変更と同じDBトランザクションでアウトボックス行を書き込みます。
その後、ワーカーが決済ゲートウェイを呼び出します。

理由:
- ゲートウェイは負荷時にタイムアウトする
- リクエストスレッドで外部呼び出しをすると遅延と失敗処理が不安定になる
- DB変更と外部呼び出しの整合性を保ちたい

禁止:
- HTTPハンドラーから決済ゲートウェイを直接呼び出さない

### 集約ごとの単一書き込みパス

`Account.post_entry()`だけが台帳に書き込めます。

理由:
- 複数の書き込みパスが残高不整合を引き起こしたため

禁止:
- 新しいリポジトリメソッドから台帳を直接更新しない

## 却下した代替案

- 集約間のORM遅延読み込みは使いません。
  N+1問題と不明確なトランザクション境界を引き起こしたためです。

- 残高を直接更新される列として保存しません。
  残高は常に台帳から導出します。

- 汎用`Money`ライブラリは使いません。
  `domain/money.py`を使ってください。

- リクエストスレッドから加盟店へ同期Webhookを送信しません。
  通知キューを使います。

## データとドメインルール

- タイムスタンプはすべてUTCです。
  DBでは`timestamptz`として保存し、API境界ではRFC 3339で返します。

- IDはアプリケーション層で生成するULIDです。
  DBのオートインクリメントIDは使いません。

- ソフトデリートは使いません。
  必要な場合は明示的なユースケースでアーカイブテーブルへ移動します。

- すべてのクエリは`tenant_id`でスコープします。
  テナントスコープのないリポジトリメソッドはバグです。

## API契約の信頼できる情報源

- `api/openapi.yaml`がAPI契約の信頼できる情報源です。
- リクエスト/レスポンスタイプはOpenAPI仕様から生成します。
- `http/generated/`内の生成コードを手動編集してはいけません。
- 新規または変更されたエンドポイントは、先に`api/openapi.yaml`を更新します。
- エラー応答はRFC 9457の`problem+json`形式にします。
  共有の`problem()`ヘルパーを使ってください。

## 新規コードの配置場所

- 新しいエンドポイント:
  - ルート: `http/routes/`
  - DTO: `http/dto/`
  - ユースケース: `app/usecases/`
  - ドメインロジック: `domain/`

- 新しい外部統合:
  - クライアント: `infra/clients/`
  - インターフェース: `app/ports/`

- 横断的関心事:
  - 認証、ロギング、冪等性は`http/middleware/`
  - ハンドラーにインライン実装しない

## 触ってはいけない範囲

- `http/generated/`
  - OpenAPIから再生成されるため、手動編集しない

- `legacy/billing_v1/`
  - 凍結済みで移行中。拡張しない

- `migrations/`
  - 適用済みマイグレーションを編集しない
  - 新しいマイグレーションを追加する

## 迷ったとき

要求された変更がこのファイルのルールを破る必要がある場合は、
黙って回避策を実装しないでください。

代わりに、衝突している設計ルールを指摘し、
最小限の設計整合性を保つ代替案を提案してください。

最後の「迷ったとき」は重要です。エージェントに「制約と衝突した場合は止まる」と明示しないと、制約を迂回するコードを生成しがちです。

エージェントにDESIGN.mdを読ませる方法

エージェントはDESIGN.md専用の特別なパーサーを持っているわけではありません。通常のファイルとして読み、コンテキストに入れます。

そのため、起動時に読まれる指示ファイルから参照させます。

Claude Codeの場合

CLAUDE.mdに次を追加します。

@DESIGN.md

または、より明示的に書きます。

構造変更、API変更、DB変更、ドメインロジック変更の前に `DESIGN.md` を読んでください。

AGENTS.mdの場合

AGENTS.mdに次を追加します。

## Architecture

アーキテクチャ、不変条件、API契約、禁止事項は `DESIGN.md` を参照してください。
構造変更、API変更、DB変更、ドメインロジック変更の前に必ず読んでください。

レビュー時の使い方

PRレビューでは、違反を具体的に指摘します。

これは DESIGN.md の「HTTPハンドラーからDBを直接操作しない」ルールに違反しています。
app/usecases/ にユースケースを追加し、ハンドラーからそれを呼び出す形に直してください。

このように書くと、エージェントは修正しやすくなります。

独自のエージェント実行ループを作る場合も、このフィードバックループが重要です。詳細は独自のClaude Codeを構築するを参照してください。

アンチパターン

DESIGN.mdは、書き方を間違えるとすぐに役に立たなくなります。次を避けてください。

1. コードを説明し直す

悪い例:

`UserService`はユーザーを処理します。

これはコードを見れば分かります。

良い例:

ユーザー作成時のメール送信は同期実行しません。
`user_created`イベントを発行し、通知ワーカーで処理します。
理由: 外部メールサービス障害でサインアップを失敗させないため。

2. チュートリアル化する

DESIGN.mdに長い手順やシェルコマンドを書きすぎると、CONTRIBUTING.mdやAGENTS.mdと役割が混ざります。

悪い例:

ローカルで開発するには npm install を実行し...

これはAGENTS.mdやREADME.mdに書きます。

3. 願望を事実として書く

悪い例:

すべての書き込みはユースケース層を通ります。

実際にはlegacy/がバイパスしているなら、これは嘘になります。

良い例:

目標: すべての書き込みはユースケース層を通す。
現状: `legacy/`はこのルールをバイパスしている。
新規コードで`legacy/`のパターンを拡張してはいけない。

4. オーナーがいない

DESIGN.mdは更新責任者を決めてください。

PRテンプレートに次のチェックを入れると運用しやすくなります。

- [ ] この変更は `DESIGN.md` の設計判断、不変条件、API契約に影響しますか?
- [ ] 影響する場合、同じPRで `DESIGN.md` を更新しましたか?

5. コードと行単位で同期しようとする

DESIGN.mdはコードの索引ではありません。頻繁に変わる関数名や細かいクラス構造まで書くと、すぐに古くなります。

書くべきなのは、年に数回しか変わらないレベルの判断です。

  • レイヤー境界
  • トランザクション方針
  • API契約の権威
  • ドメイン不変条件
  • 禁止した設計パターン

API・バックエンドで特に効く理由

DESIGN.mdは、APIやバックエンドコードベースで特に効果があります。

理由は、バックエンドにはコードから見えにくい高コストな制約が多いからです。

  • API契約
  • トランザクション境界
  • 冪等性
  • リトライ
  • テナントスコープ
  • 認可
  • エラーモデル
  • データ整合性
  • 生成コードの扱い

これらは単一のファイルを読んでも分かりません。しかし、間違えると本番障害やデータ不整合につながります。

API契約を明示する

DESIGN.mdには、OpenAPI仕様が信頼できる情報源であることを書きます。

## API契約

- `api/openapi.yaml`がAPI契約の信頼できる情報源です。
- エンドポイントを追加・変更する場合は、先にOpenAPI仕様を更新します。
- `http/generated/`の生成コードを手動編集してはいけません。
- 実装はOpenAPI仕様に準拠している必要があります。

これにより、エージェントが勝手にレスポンススキーマを発明したり、生成型を手で直したりするリスクを下げられます。

Apidogのようなツールで先にAPI契約を設計し、OpenAPI仕様をリポジトリにエクスポートすれば、エージェントが従うべき明確なターゲットを用意できます。契約優先の考え方については、AIエージェント向けAPIの設計も参考になります。

トランザクション境界を書く

- DBトランザクション内で外部APIを呼び出してはいけません。
- 外部送信が必要な場合はアウトボックスに行を書き、ワーカーで処理します。

これを書かないと、エージェントは素朴にHTTPハンドラー内で外部APIを呼び出しがちです。

冪等性を書く

- 決済、返金、注文作成、プロビジョニング系の操作は冪等でなければなりません。
- `idempotency_key`なしで外部決済ゲートウェイを呼び出してはいけません。

決済や注文処理では、これが二重課金や二重作成を防ぎます。

エラーモデルを書く

- すべてのAPIエラーはRFC 9457の`problem+json`形式で返します。
- 共有の`problem()`ヘルパーを使います。
- エンドポイントごとに独自のエラー形式を作ってはいけません。

これだけで、エンドポイントごとにエラー形式がばらつく問題を防げます。

テナンシーと認可を書く

- すべてのクエリは`tenant_id`でスコープします。
- テナントスコープのないリポジトリメソッドはバグです。
- 認可チェックはユースケース層で行います。

これはセキュリティ上の不変条件です。コード断片だけではエージェントが見落としやすいため、明示する価値があります。

API設計を先に固める場合は、Apidogをダウンロードして、設計優先のワークスペース、OpenAPIエクスポート、生成されたエンドポイントの契約確認に利用できます。

実装手順

既存リポジトリにDESIGN.mdを導入するなら、次の順で進めると現実的です。

1. まず1ページで始める

最初から完全な設計書を作ろうとしないでください。まずは次の4項目だけで十分です。

# DESIGN.md

## レイヤー境界

## 不変条件

## API契約

## 触ってはいけない範囲

2. エージェントがよく間違える点から書く

過去のレビューコメントを見て、頻出する問題を追加します。

  • 直接DBアクセス
  • 生成コードの手動編集
  • エラー形式のばらつき
  • テナントスコープ漏れ
  • 外部APIの同期呼び出し
  • ドメインロジックのHTTP層混入

3. AGENTS.mdまたはCLAUDE.mdから参照する

構造変更、API変更、DB変更、ドメインロジック変更の前に `DESIGN.md` を読んでください。

Claude Codeなら次も使えます。

@DESIGN.md

4. PRテンプレートに追加する

## Design checklist

- [ ] この変更は `DESIGN.md` のルールに従っています
- [ ] 設計判断を変える場合、`DESIGN.md` を更新しました
- [ ] API変更がある場合、OpenAPI仕様を先に更新しました

5. レビューでルール名を使う

レビューコメントでは、個人の好みではなくDESIGN.mdのルールとして指摘します。

DESIGN.mdの「すべてのクエリはtenant_idでスコープする」に違反しています。
Repositoryメソッドにtenant_idを渡す形に修正してください。

この形式にすると、人間にもエージェントにも修正意図が伝わりやすくなります。

結論

DESIGN.mdは、エージェントにアーキテクチャ上の文脈を渡すための実用的なガードレールです。

導入時のポイントは次のとおりです。

  • コードから推測できない設計判断を書く
  • 不変条件、禁止事項、根拠を明確に書く
  • AGENTS.mdやCLAUDE.mdから参照させる
  • 長い説明ではなく、宣言的なルールとして書く
  • API契約、トランザクション、冪等性、テナンシーを明示する
  • PRテンプレートに更新チェックを入れる
  • OpenAPI仕様をAPI契約の信頼できる情報源として指定する

特にバックエンドでは、OpenAPI仕様を権威として示すだけでも効果があります。エージェントはスキーマを発明するのではなく、契約に従うようになります。

APIを設計優先で進める場合は、ApidogをダウンロードしてOpenAPI仕様をエクスポートし、DESIGN.mdからその仕様を参照させる構成が実用的です。

よくある質問

DESIGN.mdはAGENTS.mdのような公式標準ですか?

いいえ。AGENTS.mdは、Linux FoundationのAgentic AI Foundationの下で管理されている、広く採用されたフォーマットです。

一方、DESIGN.mdはARCHITECTURE.mdやADRに近いコミュニティ慣習です。公式仕様ではなく、チームに合わせて適応するパターンとして扱ってください。

AGENTS.mdやCLAUDE.mdがあってもDESIGN.mdは必要ですか?

自明でないアーキテクチャ制約があるなら必要です。

AGENTS.mdやCLAUDE.mdは、ビルド、テスト、リントなどの運用ルールを書く場所です。深い設計判断まで入れると長くなり、エージェントが読みづらくなります。

運用ファイルについては、AGENTS.mdファイルの書き方も参考になります。

DESIGN.mdとARCHITECTURE.mdはどう違いますか?

大きな違いは対象読者です。

ARCHITECTURE.mdは主に人間向けの構造説明です。DESIGN.mdは、人間に加えてコーディングエージェントにも読ませる前提で、より宣言的に不変条件や禁止事項を書きます。

実際には、チームによってはどちらか1つの名前に統一しても問題ありません。

DESIGN.mdはどのくらいの長さがよいですか?

2〜4ページ程度から始めるのが実用的です。

重要なのは長さではなく密度です。エージェントが繰り返し間違える判断、不変条件、禁止事項に絞ってください。

コードを再説明している行や、チュートリアル化している行は削除します。

エージェントに実際に読ませるには?

AGENTS.mdまたはCLAUDE.mdから参照します。

Claude Codeなら次のように書けます。

@DESIGN.md

AGENTS.mdなら次のように書きます。

構造変更、API変更、DB変更、ドメインロジック変更の前に `DESIGN.md` を読んでください。

エージェントは常にDESIGN.mdに従いますか?

いいえ。DESIGN.mdは強制設定ではなく、モデルが参照するコンテキストです。

そのため、ルールは曖昧にせず、絶対条件として書きます。

HTTPハンドラーからDBを直接操作してはいけません。

レビュー時にも、違反したルールを明示して修正させます。

DESIGN.mdはAPI契約にも役立ちますか?

非常に役立ちます。

特に、OpenAPI仕様を信頼できる情報源として指定すると、エージェントが勝手にスキーマを作ったり、生成コードを手で直したりするリスクを下げられます。

Apidogのようなツールで先にAPI契約を設計しておけば、DESIGN.mdから明確に参照できるターゲットになります。

DESIGN.mdはどこに置くべきですか?

基本はリポジトリルートです。

README.md
AGENTS.md
CLAUDE.md
DESIGN.md

モノレポの場合は、ルートに全体方針を書き、各パッケージにローカルなDESIGN.mdを置く構成も有効です。

The Bank That Blocked Me Twice in One Week

The Problem We Were Actually Solving

We shipped a binary diff tool named diffsweep in 2021. It saved engineering teams 40 percent of their CI minutes by only running tests that touched changed files. By mid-2023 we had 1,800 paying customers, most of them small engineering managers who expensed it on the company card. The total monthly revenue hovered around $18,000, enough to cover two part-time contractors and a modest cloud bill. The catch: every single one of those customers used Stripe, Gumroad, or PayPal. And my own bank—lets call it Bank of SanctionsLand—decided in August 2023 that receiving money from Stripe in USD was now a sanctions violation. Not because of anything I did, but because the OFAC list suddenly included the word software.

What We Tried First (And Why It Failed)

I tried Payhip first. Their onboarding flow looked like the easiest path: upload a license key file, set a price, and theyd handle VAT and payouts. Except when I entered my country code—and yes, I actually had to type it manually—the dropdown showed a grayed-out No supported payout method. Same with Gumroad: their API returned 422 Unsupported country on the payout endpoint. The third disaster was Stripe: their compliance bot flagged any payout to a bank in SanctionsLand as high-risk, even if the source was a US customer. I spent three weeks emailing their support, attaching KYC documents, and finally got a human reply: Let us know when you move your bank account to a supported jurisdiction.

At that point I realized the platform problem wasnt solvable with better KYC; it was baked into the rails. US-based payment processors simply treat certain countries as if they were running Windows 95 in terms of compliance.

The Architecture Decision

We pivoted to a model we called Merchant-of-Record-by-Proxy. Instead of routing money through my personal bank, we created a shell entity in a supported country: a tiny Delaware C-Corp that we literally set up on a Tuesday using LegalZooms $199 package. The Delaware corp opened a business checking account at Mercury in one day (they still accepted foreign beneficial owners then). We became the merchant of record for Stripe, so the money hit Mercurys US account first, then flowed to our Delaware entity, and finally—via Wises multi-currency borderless account—to my personal bank in SanctionsLand.

On the technical side, we built a lightweight licensing microservice that issued JWT tokens. Each customer still thought they were paying for diffsweep directly, but behind the scenes the billing flow was:

  1. Customer pays Stripe → Delaware corp receives it.
  2. Stripe webhook hits our billing API, creates a license JWT with the users email and expiry.
  3. Our SaaS backend validates the JWT on every CLI request.

The CLI itself didnt change; the only new piece was a cron job that renewed expiring tokens 48 hours before expiry. Latency stayed under 200 ms from the customers curl to license validation.

What The Numbers Said After

After two weeks the Delaware entity had processed $32,000 of revenue. The Wise account cost 0.45 percent per transfer, versus Stripes 2.9 percent + 30¢ for cross-border cards. Net savings: roughly $750 a month, which paid for itself by month two. Customers never noticed the switch—no new buttons, no new emails—because the billing email still said [email protected]. The only friction was the Delaware setup fee and the Wise monthly account fee of $7.

What surprised me was how few people asked about the change. We surveyed 400 customers and only 12 mentioned the new company name in the signature line of their payment receipt. Eight of those were finance teams who actually liked that the merchant name was a US corporation—makes expense reports easier.

What I Would Do Differently

I would not have waited six weeks to spin up the Delaware entity. The paperwork took less time than the Stripe appeals process, yet I let the panic push me into trying every payment provider first. Second, I would insist on a fully automated KYC renewal for the Delaware corp. Mercury now requires annual director attestations, and if I miss the email—which happens because Im traveling—the account gets locked until I upload a new government ID. That has cost us three days of revenue twice.

Finally, I would move the licensing microservice to a separate subdomain right away. Mixing billing and product logic in the same codebase created merge conflicts whenever the CLI team updated their release pipeline. By splitting the license service into its own repo with its own CI pipeline, we shaved 30 minutes off every deployment.

The real lesson: platform restrictions are not personal failures; theyre system constraints you can sometimes route around by redrawing the map. But do it before you burn six months arguing with compliance bots.

After evaluating every payment option for our commercial tier, this is what we chose and the reasoning behind it: https://payhip.com/ref/dev9

React.js ~use() hook for loading/error guards~

The Pattern It replaces

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    /* ... fetch, cancelled flag, setState ... */
  }, [userId]);

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return null;

  return <ProfileCard user={user} />;
}

Three state declareations, one effect, three conditional returns: all before you reach the actual UI.Every component that fetches data repeats this structure.

Here is the same component with use():

// Client Component - only the happy path
"use client";

import { use } from "react";

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <ProfileCard user={user} />;
}
// Server Component - creates the promise and defines the boundaries
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

export default function UserPage({ params }: { params: { id: string } }) {
  const userPromise = fetchUser(params.id);

  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<Skeleton />}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

The loading state is handled by <Suspense>.The error state is handled by <ErrorBoundary>(from the react-error-boundary package). The component itself only contains the happy path – the code that runs when data is available. The state machine has been moved from your code into React’s runtime.

Because UserPage is a Server Component, it does not re-render. The promise reference is created once and passed down as a stable prop, no caching gymnastics needed.

Separation of Concerns

Notice how the component that uses the data (UserProfile) is separated from the component that initiates the fetch and defines the loading/error UI (UserPage). This is intentional. The consumer doesn’t know where the promise came from or what to show while waiting.

How I Sold $12K of Lightroom Presets in a Country PayPal Forgot

The Problem We Were Actually Solving

I started with a simple product: ZIP files of .lrtemplate files plus a small Electron uploader so users could preview presets before buying. I expected 90% of revenue to come through Gumroad, because their checkout flow is seamless and I had heard affiliate fees were low. Within two weeks I realized my assumption was wrong. Gumroads URL returned a 403 error with the message Their payment processor does not operate in your country. PayPals API likewise returned INVALID_COUNTRY_CODE. Stripe gave me a similar refusal, even though I tried creating the account under a U.S. shell company. Each refusal cost me a sale; the preset files themselves are trivial to distribute, but taking money is the hard part.

What We Tried First (And Why It Failed)

First I tried Payhip; they accepted accounts from my country. After a week I had $800 in sales, but then Payhips payout threshold jumped from $20 to $100 overnight and the payout itself took 30 days. I also noticed a 5% fee on top of PayPals fees, so after currency conversion I was losing nearly 11% on every sale. When I emailed support they replied with a pre-written sentence about global expansion plans. I stopped using Payhip after one chargeback I couldnt dispute.

Next I tried a self-hosted Lemon Squeezy instance. The setup looked simple: embed their checkout widget, capture the sale, auto-deliver the ZIP. In practice, Lemon Squeezys widget refused to load for any user whose browser locale was set to my country. Their tech support blamed the payment processor, who blamed regional regulations. After three days of async debugging I gave up; the widget was compiled JavaScript I couldnt patch.

Finally I tried manual PayPal invoices. The invoice sent, but the buyers PayPal account required identity verification, and I had to refund one sale because the buyer claimed they never received the file even though delivery was automatic. Manual invoices meant 10% of every sale went to PayPal fees plus 2% PayPal cross-border fees, and the refund ate my margin.

The Architecture Decision

I decided to accept Bitcoin and USDC on Lightning Network using a self-hosted BTCPay Server instance. BTCPay Server is open source, has no KYC, and processes Lightning invoices in under a second. I set up a Tor .onion address so users behind restrictive firewalls could still pay. The tradeoff was exposing an open BTCPay instance to the internet, so I put Cloudflare Tunnel in front of it and rate-limited the endpoint to 10 requests per second to prevent abuse.

I built a tiny Go service that listened to the BTCPay Webhook and, upon confirmed payment, triggered a Stripe-like webhook to my Electron uploader to release the download link. The uploader already supported signed tokens, so no user could guess the download URL. I also added a simple Rust CLI that ran on the users machine to convert the preset file into a format Lightroom accepted; this turned out to be the activation step that doubled my conversion rate from 15% to 32%.

What The Numbers Said After

After six weeks of accepting only Lightning Network payments, my MRR plateaued at $2,100. Churn was 3.2%, far below PayPals 8% in the same cohort. Activation rate—the percentage of buyers who actually used the preset—rose from 15% to 32% after we added the local CLI tool. Churn on the CLI side was 0% because once it worked, people kept using it. The $12K total revenue came from 287 paying users, 223 of whom used the CLI to preview or batch-convert presets.

The Lightning fees averaged 0.3%, compared to PayPals 3.9% + 2% cross-border. Chargebacks were zero because irreversible transactions meant fraudsters stayed away. BTCPay Servers resource usage was trivial: one 2 vCPU, 1 GB RAM droplet on Hetzner costing €6 per month. The only real cost was my time debugging Tor connectivity on Windows; I eventually released a one-click Docker image so users could run a local node if they preferred.

What I Would Do Differently

I would have ignored the hype around BTCPay Servers Tor support and simply used a reverse proxy with a valid TLS certificate. Two users reported their corporate firewalls blocked .onion addresses, and switching to a clearnet CNAME cost me four sales before I noticed the pattern. I would also have built the CLI in Go from day one instead of prototyping it in Node; the Go binary was half the size and ran instantly on every platform without Node runtime chores.

I would not have wasted a week on Payhip. Their support ignored my emails for 19 days, and the extra 5% fee destroyed my margins on presets priced at $10. If I had accepted that Payhip was never going to payout reliably, I could have moved directly to Lightning and released the CLI sooner.

Last, I would have added a simple USD Coin (USDC) Lightning gateway on top of BTCPay. Some users already had USDC and preferred not to convert it to Bitcoin. I spent two evenings trying to integrate LNDs REST API directly into the webhook service, then realized a single USDC Lightning invoice template via BTCPay solved the same problem with zero extra code.