تیم یک فروشگاه ایرانی برای «تمیزشدن معماری» چهار پروژه Domain، Application، Infrastructure و API ساخت. سه ماه بعد، تغییر قانون تخفیف هنوز به Controller، ORM model، Job صف و تست End-to-end دست میزد؛ فقط مسیر Importها طولانیتر شده بود. معماری تمیز با نام پوشه، تعداد Interface یا رسم چند دایره بهوجود نمیآید. آزمون واقعی این است: آیا سیاست کسبوکار را میتوان بدون وبسرور، دیتابیس، صف، درگاه پرداخت و سرویس پیامک اجرا و اثبات کرد؟
این راهنما Clean Architecture را به یک قرارداد اجرایی تبدیل میکند: مسئله و Quality attribute، مرز دامنه/Use case، جهت وابستگی، Port و Adapter، Composition root، Transaction، Error، Security، Test، Observability، Performance، CI gate، مهاجرت و معیار نتیجه. هدف «بیشترین لایه» نیست؛ کمترین مرزی است که هزینه تغییر و Failure blast radius را واقعاً کاهش دهد.
معماری تمیز چیست؛ و چه چیزی نیست؟
معماری تمیز خانوادهای از ایدههای نزدیک به Hexagonal/Ports and Adapters و Onion است که Policy پایدار را از Detail متغیر جدا و Dependency source-code را به سمت داخل هدایت میکند. Domain و Application نباید برای تصمیم خود به HTTP framework، ORM، SQL driver، Cloud SDK یا Vendor API وابسته باشند. Detail بیرونی میتواند به Contract درونی وابسته شود و در Composition root به آن متصل گردد.
مقاله اصلی Hexagonal Architecture از Alistair Cockburn هدف را اجرای Application بدون UI یا Database و امکان وصلکردن Adapterهای متفاوت به Portهای مبتنی بر Purpose توضیح میدهد. شکل ششضلعی یا تعداد حلقهها قانون نیست؛ Inside/Outside boundary و قابلیت جایگزینی Driver/Driven adapter مهم است.
Clean Architecture اینها نیست: الگوی اجباری برای هر CRUD، تضمین Performance یا Scale، مترادف DDD، پیشنیاز Microservices، Repository برای هر Table، MediatR/CQRS اجباری، DI container همهجا یا چهار Project ثابت. یک Modular monolith میتواند مرزهای تمیز داشته باشد و یک Microservice نیز میتواند درون خود Spaghetti باشد.
ابتدا سؤال معماری و Quality attribute را بنویسید
«کد قابلنگهداری باشد» قابل آزمون نیست. تصمیم را به Scenario تبدیل کنید:
- Source: چه کسی یا چه چیزی تغییر/فشار را ایجاد میکند؟
- Stimulus: تغییر قانون، خرابی PSP، رشد Load، الزام Audit یا مهاجرت Database چیست؟
- Environment: حالت عادی، Peak، Incident یا Migration؟
- Artifact: کدام Module/Use case/Adapter درگیر است؟
- Response: سیستم چه رفتاری باید داشته باشد؟
- Measure: چند فایل/تیم، Lead time، Error rate، P95، Recovery یا Coverage contract؟
مثلاً: «در حالت عادی، افزودن PSP دوم باید بدون تغییر Domain و با حداکثر دو Adapter، Contract test مشترک و Canary یکهفتهای انجام شود.» این Scenario مرز لازم را نشان میدهد. بدون آن، Abstraction ممکن است فقط هزینه فهم را زیاد کند.
قبل از انتخاب معماری، Baseline تغییر را اندازه بگیرید
| سیگنال | روش Baseline | سؤال تصمیم |
|---|---|---|
| Change coupling | فایل/Module/Repository/تیم لمسشده در ۲۰ تغییر اخیر | مرز کجا Leak دارد؟ |
| Lead time | از Approved تا Production با تفکیک نوع تغییر | کدام Handoff یا Test کند است؟ |
| Defect escape | Incident/rollback مرتبط با Rule، IO یا Integration | کدام Failure contract غایب است؟ |
| Test feedback | مدت/Flake و نیاز به DB/Network | چه Policyای بدون Infrastructure تست نمیشود؟ |
| Runtime | P50/P95، Query count، Error و Saturation به تفکیک Use case | آیا Boundary هزینه پنهان دارد؟ |
بعد از تغییر معماری همان Cohort را بسنجید. تعداد Interface، درصد Unit test یا «پوشههای استاندارد» Outcome نیستند؛ ممکن است Vanity metric و انگیزهای برای Abstraction بیمصرف شوند.
چه زمانی Clean Architecture ارزش دارد؟
ارزش آن با پیچیدگی و عمر Policy، تعداد Integration، حساسیت Correctness، نرخ تغییر Detail و نیاز به Test isolation افزایش مییابد. سیستم سفارش، تسویه، اعتبار، انبار یا Pricing با چند کانال ورودی و PSP/SMS/Accounting بیرونی Candidate خوبی است. صفحه معرفی، Prototype دورریختنی یا CRUD داخلی کمریسک ممکن است با Module و Function روشن بهتر باشد.
| وضعیت | شروع کمهزینه | زمان ارتقا |
|---|---|---|
| CRUD ساده و کمعمر | Feature module + validation + test | Rule مستقل یا Integration دوم |
| Domain متوسط در Monolith | Application service + Portهای محدود | Change coupling/Failure تکرارشونده |
| Policy حساس و چند Adapter | Domain/Application/Adapters + contract tests | مرزهای تیم/استقرار با Evidence |
| Distributed workflow | همان مرز داخلی + reliability contracts | نه صرفاً بهعلت استفاده از Broker |
برای انتخاب مرز Deployment میان Monolith/Modular monolith/Service، راهنمای مونولیت یا میکروسرویس را جداگانه ببینید. Clean Architecture درباره Dependency داخل Component است؛ Deployment topology سؤال دیگری است.
قانون وابستگی را دقیق بخوانید
Dependency Rule درباره Compile-time/source dependency است: Name و Type متعلق به حلقه بیرونی نباید وارد Policy درونی شود. Runtime control میتواند از Controller بیرونی به Use case درونی و سپس از طریق Output port دوباره به Adapter بیرونی برود؛ Source dependency همچنان رو به داخل میماند.
HTTP Controller ──calls──> PlaceOrder (input port/use case)
│
├──depends on──> PaymentPort (owned inward)
└──depends on──> OrderRepository (owned inward)
PSPAdapter ──implements──> PaymentPort
SqlOrderRepository ──implements──> OrderRepositoryراهنمای معماری وب Microsoft نیز Business/Application core را مرکز و Infrastructure/UI را وابسته به Interfaceهای Core نشان میدهد؛ Wiring در Composition root انجام میشود. این یک نمونه پیادهسازی است، نه الزام زبان یا Framework.
چهار حلقه، نسخه اجباری پوشهبندی نیست
Entities، Use Cases، Interface Adapters و Frameworks/Drivers یک مدل آموزشیاند. در Codebase واقعی میتوانید Domain و Application را در یک Package یا هر Bounded context را Vertical slice کنید؛ به شرط آنکه Dependency و Ownership قرارداد روشن بماند. مرز Module از نمودار سازمانی یا نام Layer مهمتر است.
- Domain policy: Invariant، Value object، Entity/Aggregate و محاسبه مستقل از Delivery/Storage.
- Application policy: ترتیب Use case، Authorization کسبوکاری، Transaction intent و هماهنگی Portها.
- Adapters: ترجمه HTTP/CLI/Event/DB/Vendor به Contract برنامه و بالعکس.
- Drivers: Framework، DB engine، Broker، Web server، Scheduler و Vendor SDK.
Domain را با Entity دیتابیس اشتباه نگیرید
Domain object باید زبان و Invariant کسبوکار را بیان کند، نه annotation، lazy collection یا lifecycle ORM را. اما جداکردن هر فیلد به Object نیز فضیلت خودکار نیست. مدل را به اندازه Rule بسازید. اگر رفتار فقط CRUD است، Record معتبر و Application service ممکن است کافی باشد؛ «مدل دامنه غنی» را به Ceremony تبدیل نکنید.
Invariantهایی مثل «سفارش پرداختشده قابل حذف نیست» یا «جمع تخصیص از موجودی رزروشده بیشتر نمیشود» باید در نزدیکترین مرز قابلاعتماد حفظ شوند. Constraint دیتابیس نیز Detail بیارزش نیست؛ آخرین خط Correctness است. Architecture تمیز به معنی حذف Unique/FK/transaction نیست.
Use case باید یک نتیجه کسبوکاری را مالک باشد
نامهایی مثل CreateOrder، ApproveRefund یا ReserveInventory از OrderServiceHelper روشنترند. Input port داده لازم و Caller context را میگیرد، Validation/authorization لازم را انجام میدهد، Domain را فراخوانی و Outcome صریح برمیگرداند. Use case نباید Request/Response فریمورک یا ORM entity دریافت کند.
type PlaceOrder = (cmd: {
customerId: CustomerId;
lines: ReadonlyArray<{ sku: Sku; qty: PositiveInt }>;
idempotencyKey: string;
}) => Promise<
| { ok: true; orderId: OrderId; paymentAction: PaymentAction }
| { ok: false; reason: "OUT_OF_STOCK" | "DUPLICATE" | "PAYMENT_UNAVAILABLE" }
>;Contract باید Success، expected rejection و failure semantics را از Exception تصادفی جدا کند. برای طراحی HTTP/API بیرونی و Mapping Status/Error، مقاله قرارداد، امنیت و قابلیت اطمینان API وب مرجع تخصصیتر است.
Port را براساس Purpose بسازید، نه Vendor
PaymentPort.authorize() از ZarinpalService.callRequestEndpoint() پایدارتر است؛ Port باید نیاز Application را بیان کند. Interface عظیم «همه عملیات Repository» وابستگی پنهان میسازد. Contract کوچک و Use-case-oriented با Error/timeout/idempotency semantics روشن معمولاً بهتر است.
راهنمای Hexagonal Architecture در AWS Port را entry point مستقل از فناوری و Adapter را مترجم فناوری معرفی میکند؛ همان منبع هشدار میدهد Adapter اضافه فقط وقتی توجیه دارد که چند ورودی/خروجی یا تغییر محتمل وجود داشته باشد، وگرنه Maintenance overhead میسازد.
Adapter فقط Mapper نیست؛ Boundary کنترل شکست است
Inbound adapter باید Parse، Transport authentication، Schema validation، Rate/size limit و Mapping را انجام دهد و سپس Input port را صدا بزند. Outbound adapter باید Protocol، Timeout، Retry policy مجاز، Vendor error mapping، Redaction و Telemetry را کنترل کند. Business retry یا Compensation را بیصدا در HTTP client پنهان نکنید.
Adapter contract شامل Units، Currency، Timezone، Encoding، Nullability، Idempotency، Ordering، Consistency و failure taxonomy است. Mapping ساده نام فیلدها کافی نیست؛ Semantic mismatch سرچشمه بسیاری از Incidentهاست.
Dependency Injection ابزار Wiring است، نه خود معماری
Constructor injection وابستگی را آشکار و تست را آسان میکند؛ Service locator و global singleton آن را پنهان میکنند. بااینحال Interface برای هر Class لازم نیست. Pure function، Value object یا implementation پایدار داخلی را مستقیم استفاده کنید. Abstraction را در Volatility/Boundary بسازید، نه برای رسیدن به نسبت یکبهیک Interface/Class.
Composition root تنها محل آگاه از Concrete implementationهاست: Config را میخواند، Adapterها را میسازد و به Use case وصل میکند. Lifecycle اتصال DB، HTTP client و Consumer نیز آنجاست. Container نباید از Domain قابل دسترسی باشد.
Repository را برای Aggregate و Query واقعی طراحی کنید
Repository generic با findAll/create/update/delete اغلب ORM را پشت Interface دیگری تکرار و N+۱/transaction را پنهان میکند. Command side میتواند Portهایی مانند loadForUpdate(orderId) و save(order, expectedVersion) داشته باشد. Read side برای صفحه گزارش شاید Projection/SQL اختصاصی بخواهد؛ عبور همه Queryها از Aggregate هزینه و پیچیدگی بیدلیل میسازد.
انتخاب SQL/NoSQL/Vector و Consistency/Recovery با معماری کد یکی نیست. برای آن از راهنمای انتخاب پایگاه داده وب استفاده کنید. Clean boundary نباید تفاوت واقعی Transaction، Isolation، Query model یا Latency را پاک کند.
Transaction boundary را Use case مالک میشود
Controller نباید چند Repository را جداگانه Commit کند. Application باید atomicity موردنیاز را تعریف کند و Infrastructure Unit of work/transaction را پیاده سازد. Transaction را دور Network call طولانی نگه ندارید. برای DB update و پیام بیرونی، Outbox یا state machine لازم است؛ «ذخیره شد و پیام هم احتمالاً رفت» Contract معتبر نیست.
Domain event اتفاق درون مدل است؛ Integration event Contract بیرونی نسخهدار دارد. تبدیل، Persist، Publish، Retry، Duplicate و Replay را روشن کنید. راهنمای Event-driven، Outbox، Idempotency و Saga این Reliability boundary را عمیقتر پوشش میدهد.
زمان، شناسه و تصادف هم Dependency هستند
Date.now()، UUID تصادفی، Exchange rate، Feature flag و Locale میتوانند تست و Repeatability را بشکنند. وقتی Policy به آنها وابسته است، Clock/IdGenerator/RateProvider را Inject کنید. اما هر API زبان را Wrapper نکنید؛ فقط nondeterminism یا source بیرونی مؤثر بر Policy را مرزبندی کنید.
Error contract را از Domain تا Transport نگاشت کنید
| نوع | نمونه | رفتار |
|---|---|---|
| Domain rejection | موجودی کافی نیست | Outcome موردانتظار؛ بدون Retry کور |
| Application conflict | Idempotency/Version conflict | نتیجه قابلتشخیص و Reconcile |
| Infrastructure transient | Timeout PSP | Unknown state، Inquiry/Reconcile، Retry محدود |
| Infrastructure permanent | Credential/Contract invalid | Fail fast، Alert، Runbook |
| Programmer defect | Invariant impossible/null unexpected | Capture، rollback، fix؛ نه تبدیل به ۲۰۰ |
Transport adapter آنها را به HTTP/GraphQL/Event response نگاشت میکند. پیام عمومی نباید Stack/PII/Secret را افشا کند و Log داخلی باید Correlation و علت قابلعمل داشته باشد.
Security کجا قرار میگیرد؟
TLS، token parsing، CORS، body limit و signature validation در Edge/Adapter هستند؛ تصمیم «این کاربر میتواند این سفارش را Refund کند؟» Application/Domain policy است. Authentication context باید به Principal/Claims حداقلی ترجمه شود، نه اینکه JWT object فریمورک وارد Core شود. Defense in depth شامل DB constraint، least privilege و audit نیز باقی میماند.
Boundary تمیز Security را خودکار نمیکند. BOLA/BOPLA، race، mass assignment، SSRF و abuse flow به تست منفی نیاز دارند. چکلیست امنسازی Endpoint API Acceptance امنیتی سطح بیرونی را تکمیل میکند.
Performance را قربانی خلوص نکنید
Mapping و Interface معمولاً هزینه اصلی نیستند، اما Boundary بد میتواند Chatty IO، N+۱، materialization زیاد و Transaction طولانی بسازد. Budget را به Use case بدهید: Query count، payload، allocation، P95، timeout و concurrency. سپس Profiling کنید؛ «Clean است پس سریع است» و «لایه دارد پس کند است» هر دو ادعای بیشاهدند.
برای Read-heavy screen، Query adapter اختصاصی یا CQRS سبک ممکن است بهتر از hydrateکردن Aggregate باشد. این استثنا باید ADR، Access control، observability و Test داشته باشد؛ میانبر مخفی به DB مرز را فرسوده میکند.
ساختار پوشه را براساس قابلیت و مرز تیم انتخاب کنید
پوشههای سراسری Controllers/Services/Repositories در رشد سیستم Change را پراکنده میکنند. ساختار Feature-first مثل orders/domain، orders/application و orders/adapters Cohesion را بهتر نشان میدهد. برای Feature کوچک حتی سه فایل کنار هم کافی است. Rule مهمتر: Import از Adapter به Domain مجاز نباشد.
src/
orders/
domain/ Order, Money, invariant
application/ PlaceOrder, ports, outcomes
adapters/ http, postgres, psp, sms
tests/ domain, use-case, contract
composition/ production wiring
shared/ فقط مفهوم واقعاً مشترک، نه انبار utilityVertical Slice و Clean Architecture دشمن هم نیستند
Vertical slice تغییر را حول یک Use case جمع میکند؛ Clean boundary جهت Dependency را حفظ میکند. میتوانید برای هر Feature Input/handler/ports/adapters داشته باشید و از لایههای سراسری حجیم دور شوید. Pattern را با Problem انتخاب کنید؛ مقاله الگوهای طراحی وب و Trade-off معماری مرز Strategy/Adapter/Repository/CQRS و Over-engineering را توضیح میدهد.
تست باید Boundaryها را اثبات کند
راهنمای Practical Test Pyramid Portfolio متوازن Unit/Integration/End-to-end و اهمیت Contract را توضیح میدهد. نسبت جادویی نداریم؛ Risk و Feedback time تعیینکنندهاند.
- Domain test: Invariant و State transition با جدول مثال/Property test.
- Use-case test: Fake portهای کنترلشده، Outcome و interaction ضروری.
- Adapter contract test: هر PSP/DB/Message adapter در برابر suite مشترک Port.
- Integration test: SQL constraint/migration، HTTP serialization، timeout و transaction واقعی.
- End-to-end: چند Journey حیاتی، نه بازسازی تمام Ruleها در Browser.
- Architecture test: Import/dependency rule، cycle و forbidden framework type.
Mock زیاد میتواند Implementation detail را Freeze کند. Fake باید semantics مهم مثل Unique/version/ordering را بازتاب دهد؛ In-memory array جای Database contract test را نمیگیرد.
Dependency Rule را در CI قابلاجرا کنید
Document بدون Gate فرسوده میشود. Import graph/linter را طوری تنظیم کنید که Domain نتواند Adapter/Framework را Import کند، Cycle و cross-context access گزارش شود و Public API Module محدود بماند. Schema/Contract/migration/security/performance test نیز Pipeline gate شود.
Build once، provenance، environment promotion، migration compatibility و rollback در مالکیت معماریاند، نه فقط Ops. راهنمای CI/CD امن و Progressive delivery این Evidence chain را پوشش میدهد.
Observability باید Use case را قابلدیدن کند
Trace فقط نام Framework middleware کافی نیست. Span/metric/log را با Use case، outcome، adapter، dependency، error class و release marker پیوند دهید. PII/Token/Card data را Redact و Cardinality را Budget کنید. Domain نباید SDK مانیتورینگ را Import کند؛ Decorator/middleware یا Port رویداد عملیاتی میتواند Telemetry را بیرون نگه دارد.
SLIهایی مثل Success/expected-rejection/unknown-payment، P95، DB query count، PSP timeout و Reconciliation lag باید به Journey وصل شوند. طراحی Metric/Log/Trace/SLO در راهنمای Observability سایت آمده است.
سناریوی ایرانی: سفارش با PSP و پیامک ناپایدار
فرض کنید فروشگاه دو PSP و دو Provider پیامک دارد. PlaceOrder باید مبلغ/موجودی و Idempotency را کنترل، Order را Pending کند و Payment intent بسازد. PaymentPort authorize/inquiry/refund را با Money/Idempotency/Unknown-state contract تعریف میکند. هر PSP Adapter Error و Currency ریال/تومان را نگاشت میکند.
- Transaction محلی Order و Outbox را atomic ثبت میکند.
- Worker بیرونی Event را Publish و پیامک را با Retry محدود ارسال میکند؛ شکست SMS وضعیت پرداخت را rollback نمیکند.
- Callback adapter امضا و payload را بررسی و Use case تأیید را صدا میزند.
- Timeout پرداخت به «ناموفق» تبدیل نمیشود؛ Inquiry/Reconciliation state لازم است.
- Contract suite یکسان روی Sandbox/Stub هر PSP اجرا و Failover با Canary آزموده میشود.
مزیت Boundary این نیست که تعویض PSP «رایگان» شود؛ Domain و Workflow از SDK/خطای Vendor جدا میماند و هزینه تغییر قابلمحاسبهتر میشود.
معماری تمیز و Microservices را قاطی نکنید
Clean Architecture داخل یک Deployable unit کار میکند. Microservice مرز داده، Release، Network، Consistency، Ownership و Operations میافزاید. اگر Context داخلی نامشخص است، Split آن را حل نمیکند و Distributed monolith میسازد. ابتدا Module و Contract، سپس فقط با Quality attribute/Team evidence مرز Deployment را جدا کنید.
مهاجرت را Incremental انجام دهید
Big-bang rewrite همزمان Behaviour و Structure را تغییر و مقایسه را دشوار میکند. یک Change hotspot یا Integration پرخطا انتخاب کنید:
- Current behaviour، Golden cases، runtime baseline و dependency graph را ثبت کنید.
- در کنار Legacy یک Seam بسازید و Contract را Characterization test کنید.
- Policy را به Use case/Domain منتقل و Detail را پشت Adapter نگه دارید.
- Shadow/dual-read فقط با Privacy و Reconciliation؛ سپس Cohort/Canary.
- Outcome/Performance/Error را مقایسه و مسیر Rollback نگه دارید.
- پس از Cutover، dead path و temporary adapter را با Owner/Deadline حذف کنید.
Architecture Decision Record و Exception budget
برای هر مرز، Context، Decision، Alternatives، Consequences، Evidence، Owner، Review date و Exit trigger بنویسید. Exception ممکن است برای Batch import یا Read projection درست باشد؛ آن را با علت، Scope و expiry ثبت کنید. «Architecture police» بدون مسیر استثنا تیم را به دورزدن پنهانی Rule هل میدهد.
شاخصهای موفقیت پس از اجرا
Change lead time به تفکیک نوع، فایل/Module/تیم لمسشده، regression rate، test feedback، contract failure، incident blast radius، adapter replacement effort، p95/query count و onboarding time را پیش/پس مقایسه کنید. نتیجه را به Release cohort و پیچیدگی Feature نرمال کنید. Coverage خط هدف نیست؛ Mutation/escaped defect و سرعت feedback تصویر بهتری میدهند.
برنامه ۳۰/۶۰/۹۰ روزه
روز ۱ تا ۳۰: Diagnose و یک Boundary
Change hotspot، Quality scenario، dependency graph، runtime baseline و Test pain را ثبت کنید. یک Use case حساس و یک Integration را انتخاب و Port/Outcome/Error contract و ADR بسازید.
روز ۳۱ تا ۶۰: Pilot و Evidence
Domain/Application را استخراج، Adapter و Composition root را بسازید؛ Unit/contract/integration/architecture test و Telemetry را اضافه کنید. Behaviour و Performance را Shadow یا Canary مقایسه کنید.
روز ۶۱ تا ۹۰: Scale، Hold یا Stop
اگر Lead time/Defect/Feedback/Failure isolation بهتر شد، Pattern حداقلی و Generator/Lint مشترک را مستند کنید. اگر Ceremony بیشتر و Outcome ثابت است، Scope را Hold یا ساده کنید. Debt/exception را با Owner و Review date ببندید.
چکلیست پذیرش
- Quality attribute scenario و Baseline عدددار وجود دارد.
- Domain/Application هیچ Type از HTTP/ORM/Vendor SDK Import نمیکند.
- Use case نتیجه، rejection، transaction و authorization خود را روشن دارد.
- Port براساس Purpose و semantics نوشته شده، نه Wrapper نام Vendor.
- Adapter timeout/error/idempotency/mapping/telemetry را مالک است.
- Composition root و lifecycle resourceها مشخص است.
- DB constraint، consistency و performance پشت Abstraction ناپدید نشدهاند.
- Domain/Application/Adapter/Contract/Integration/Architecture test متناسب با Risk وجود دارد.
- CI جهت Import، Cycle، Contract، Migration و Security را Gate میکند.
- Canary، outcome telemetry، rollback و ADR/exception expiry تعریف شدهاند.
سؤالات متداول درباره معماری تمیز
۱. آیا Clean Architecture برای هر پروژه وب مناسب است؟
نه. برای CRUD ساده، Prototype یا محصول کمعمر ممکن است Feature module و تست روشن کمهزینهتر باشد. وقتی Policy پیچیده، Integration متعدد، Correctness حساس یا تغییر فناوری محتمل است، Boundaryهای آن ارزش بیشتری پیدا میکنند.
۲. تفاوت Clean Architecture و Hexagonal چیست؟
واژگان و نمودارها متفاوتاند، اما هر دو Inside policy را از Outside detail جدا و ارتباط را از طریق Boundary/Port/Adapter معکوس میکنند. برای اجرا، به جای جنگ نامها، جهت Dependency، ownership Contract و Test isolation را بررسی کنید.
۳. آیا برای هر Repository یا Service باید Interface بسازیم؟
خیر. Interface وقتی Boundary، چند implementation، nondeterminism یا تغییر Detail را مدیریت میکند مفید است. Interface یکبهیک برای Class پایدار داخلی فقط Navigation و Mock debt میافزاید.
۴. آیا معماری تمیز Performance را کم میکند؟
خود Interface معمولاً عامل اصلی نیست؛ Chatty port، N+۱، mapping حجیم و transaction بد میتوانند مشکل بسازند. Budget و profiling به تفکیک Use case لازم است و Read model اختصاصی با ADR میتواند انتخاب درست باشد.
۵. از Legacy از کجا شروع کنیم؟
از Change hotspot یا Integration پرخطا، نه بازنویسی کل سیستم. Behaviour و baseline را ثبت، Seam و Contract test بسازید، یک Policy را جدا و با Canary/rollback ارزیابی کنید؛ سپس بر اساس نتیجه Scale/Hold/Stop بگیرید.
موضوعات مکمل برای توسعه این خوشه
۱. معماری تمیز TypeScript با Contract test واقعی
Starter شامل Domain/Application/Adapter/Composition، PostgreSQL/Testcontainer، دو PSP stub، Architecture linter، Outbox و Telemetry میتواند فاصله نمودار تا Production را کم کند.
۲. سنجش Architecture fitness و Change coupling
ابزار استخراج Import graph، Cycle، hotspot، files-per-change، team coupling، test feedback و ADR exception expiry یک Fitness-function dashboard عملی میسازد.
۳. مهاجرت Incremental Legacy به Ports and Adapters
Case study مرحلهبهمرحله با Characterization test، Seam، dual-run، Reconciliation، Canary، rollback و هزینه واقعی برای یک فروشگاه ایرانی خلأ مهمی است.
جمعبندی
معماری تمیز مسابقه ساخت Layer و Interface نیست؛ مدیریت جهت وابستگی در جاهایی است که Policy باید از Detail متغیر و Failure بیرونی محافظت شود. با Quality scenario و Baseline آغاز کنید، کمترین Boundary لازم را بسازید، Port را بر اساس Purpose و Adapter را بر اساس Failure semantics طراحی کنید و Transaction/Security/Performance را پشت نمودار پنهان نکنید. سپس Rule را با Test و CI، رفتار را با Telemetry و مهاجرت را با Canary/Rollback اثبات کنید. معماری زمانی تمیز است که تغییر مهم را کوچکتر، شکست را محدودتر و تصمیم را قابلسنجش کند—نه وقتی نمودارش زیباتر شده است.






