ویرگول
ورودثبت نام
علیرضا فیضی
علیرضا فیضیتوسعه دهنده بک‌اند / پایتون / گولنگ
علیرضا فیضی
علیرضا فیضی
خواندن ۱۴ دقیقه·۲ ماه پیش

معماری Code-Level Monolith؛ معماری هیبریدی و هنر "دیپلوی منعطف"

۱. توهم انتخاب و پایان دوگانگی کاذب

در دهه گذشته، صنعت توسعه نرم‌افزار شاهد نوساناتی رادیکال در پارادایم‌های معماری بوده است؛ حرکتی شتابان و گاهی هیجانی از سیستم‌های یکپارچه (Monolith) به سمت میکروسرویس‌ها (Microservices) و اخیراً، بازگشتی تأمل‌برانگیز به سمت معماری‌های یکپارچه مدرن و ساختاریافته.

برای سال‌ها، مهندسان نرم‌افزار در برابر یک دوراهی کاذب قرار گرفته بودند: یا باید "سادگی و سرعت توسعه" معماری یکپارچه را انتخاب می‌کردند و ریسک تبدیل شدن به "توپ بزرگ گِل" (Big Ball of Mud) را می‌پذیرفتند، و یا برای دستیابی به "مقیاس‌پذیری"، تن به پیچیدگی‌های کمرشکن میکروسرویس‌ها (مدیریت شبکه، کانسیستنسی توزیع‌شده و ارکستراسیون) می‌دادند.

اما آیا راه سومی وجود دارد؟

این مقاله به کالبدشکافی عمیق معماری "یکپارچه در سطح کد" (Code-Level Monolith) یا Modulith می‌پردازد. این معماری، یک تلاش مهندسی‌شده برای دستیابی به "جامِ مقدس" مهندسی نرم‌افزار است: ترکیب استقلال ماژولار میکروسرویس‌ها با سادگی عملیاتی و کارایی سیستم‌های یکپارچه.

در این رویکرد، ما با یک پارادایم شیفت مواجهیم:

"ماژولاریتی یک مفهوم منطقی (Logical) است، نه فیزیکی."

ما در این مقاله نشان خواهیم داد که چگونه می‌توان سیستمی طراحی کرد که در زمان توسعه، کاملاً ماژولار و ایزوله باشد (مانند میکروسرویس)، اما در زمان استقرار (Deploy)، بتواند بنا به نیاز، به صورت یک باینری واحد (All-in-One) یا مجموعه‌ای از سرویس‌های توزیع‌شده اجرا شود. این قابلیت "سویچ کردن بدون اصطکاک"، دقیقاً همان حلقه‌ی مفقوده‌ای است که تیم‌های مهندسی برای فرار از "پیچیدگی زودرس" به آن نیاز دارند.


۲. بازگشت عقلانیت: چرا غول‌های فناوری دنده معکوس کشیدند؟

شرکت‌های پیشرو نظیر Amazon Prime Video (که با بازگشت به یکپارچگی هزینه‌های خود را ۹۰٪ کاهش داد)، Shopify، Google و Segment، هر کدام بنا به دلایل فنی و فشارهای اقتصادی، استراتژی‌های خود را بازتعریف کرده‌اند. این بازگشت، نه از روی دلتنگی برای گذشته، بلکه پاسخی عمل‌گرایانه به پیچیدگی‌های مدیریت‌ناپذیر بود.

برای درک اینکه چرا Code-Level Monolith امروز به عنوان معماری برنده مطرح می‌شود، باید مسیر پرفراز و نشیب تکامل معماری نرم‌افزار را مرور کنیم.

۲.۱. عصر یکپارچگی سنتی و نفرینِ «توپ بزرگ گِل»

تاریخچه توسعه نرم‌افزار مدرن با سیستم‌های یکپارچه (Monolithic) آغاز شد. در این معماری کلاسیک، تمامی اجزای سیستم اعم از رابط کاربری (UI)، منطق تجاری (Business Logic) و لایه دسترسی به داده (Data Access) در یک پایگاه کد واحد (Codebase) قرار داشتند و به صورت یک واحد اجرایی (Executable) مستقر می‌شدند.

مزیت فریبنده: سادگی.

توسعه‌دهندگان به سرعت کد می‌نوشتند، تغییرات را اعمال می‌کردند و تنها با کپی کردن یک فایل روی سرور، عملیات استقرار (Deployment) به پایان می‌رسید.

اما سقوط:

با رشد سیستم‌ها و افزایش پیچیدگی، این معماری اغلب به ضدالگوی معروف «توپ بزرگ گِل» (Big Ball of Mud) تبدیل می‌شد. در این وضعیت:

  • محو شدن مرزها: خطوط بین ماژول‌های مختلف از بین می‌رفت و کلاس‌ها به شدت در هم تنیده (Tightly Coupled) می‌شدند.

  • اثر پروانه‌ای مخرب: تغییری کوچک در نحوه محاسبه مالیات، می‌توانست منجر به خطایی پیش‌بینی‌ناپذیر در بخش مدیریت موجودی انبار شود.

  • شکنندگی: ترس از تغییر کد باعث کندی توسعه و کاهش کیفیت نرم‌افزار می‌شد.

۲.۲. سرابِ میکروسرویس‌ها: وعده‌ها و هزینه‌های پنهان

در واکنش به بن‌بست یکپارچگی سنتی و با الهام از شرکت‌هایی نظیر Netflix و Uber که با چالش‌های مقیاس‌پذیری در ابعاد سیاره‌ای روبرو بودند، معماری میکروسرویس به عنوان منجی ظهور کرد.

ایده اصلی: شکستن "توپ بزرگ" به سرویس‌های کوچک و مستقلی که هر کدام مسئول یک قابلیت تجاری مشخص هستند و از طریق شبکه با هم گفتگو می‌کنند.

وعده‌های طلایی:

  • استقلال تیم‌ها: هر تیم می‌تواند با تکنولوژی و سرعت دلخواه خود کار کند.

  • مقیاس‌پذیری دقیق (Granular Scaling): اختصاص منابع بیشتر فقط به سرویس‌های پرمصرف (مثلاً سرویس جستجو).

  • ایزولاسیون خطا: خرابی در سرویس پرداخت، لزوماً کل سایت را پایین نمی‌آورد.

واقعیت تلخ:

با گذشت زمان، بسیاری از سازمان‌ها با حقیقتی دردناک روبرو شدند: «شما نتفلیکس نیستید!»

میکروسرویس‌ها پیچیدگی را از بین نبردند، بلکه طبق قانون «بقای پیچیدگی»، آن را از سطح کد (Logic) به سطح زیرساخت و عملیات (Infrastructure & Ops) منتقل کردند. چالش‌های جدید شامل موارد زیر بود:

  • تأخیر شبکه (Network Latency): جایگزینی فراخوانی‌های سریع حافظه با درخواست‌های کند HTTP/gRPC.

  • کابوس دیباگینگ: دنبال کردن یک باگ در میان ۱۰ سرویس مختلف.

  • تراکنش‌های توزیع‌شده: نیاز به الگوهای پیچیده‌ای مثل Sagas برای حفظ یکپارچگی داده‌ها.

  • هزینه سربار: نیاز به تیم‌های تخصصی DevOps صرفاً برای روشن نگه داشتن چراغ‌ها.

۲.۳. ظهور Code-Level Monolith: نقطه تعادل طلایی (۲۰۲۰ - ۲۰۲۵)

در سال‌های اخیر، رویکرد جدیدی تحت عنوان Modular Monolith یا Code-Level Monolith محبوبیت یافته است. این معماری تلاشی مهندسی‌شده برای آشتی دادنِ «سادگیِ توسعه» با «نظمِ معماری» است.

در این پارادایم:

  1. استقرار واحد (Physical Monolith): سیستم همچنان به عنوان یک واحد (Single Unit) بیلد و مستقر می‌شود (مثل پروژه Quick Connect در حالت all-in-one).

  2. ایزولاسیون کد (Logical Modularity): تمامی کدها معمولاً در یک Monorepo نگهداری می‌شوند، اما در سطح کد، ماژول‌ها به شدت ایزوله هستند و مرزهای سخت‌گیرانه‌ای (Strict Boundaries) توسط کامپایلر یا ابزارهای لینت (Lint) اعمال می‌شود.

تفاوت بنیادین:

ارتباط بین ماژول‌ها (مثلاً بین ماژول سفارش و ماژول کاربر) از طریق فراخوانی متد (Function Call) در حافظه انجام می‌شود، نه شبکه. این یعنی تأخیر صفر.

این معماری بر یک اصل کلیدی استوار است:

«مدولاریتی یک ویژگی منطقی است، نه فیزیکی.»

شما می‌توانید سیستمی کاملاً مدولار داشته باشید که در یک پروسه اجرا می‌شود (Code-Level Monolith)، و برعکس، می‌توانید سیستمی از میکروسرویس‌ها داشته باشید که به شدت در هم تنیده و وابسته هستند (Distributed Monolith).


۳. مبانی نظری: آناتومی یک «یکپارچه مدولار»

برای پیاده‌سازی موفق یک Code-Level Monolith، صرفاً "ریختن تمام فایل‌ها در یک پوشه" کافی نیست. این معماری نیازمند دیسیپلین مهندسی بالایی است که ما آن را در سه اصل بنیادین خلاصه می‌کنیم: مرزهای سخت‌گیرانه، شفافیت مکان و استراتژی داده ایزوله.

۳.۱. مرزهای سخت‌گیرانه (Strict Boundaries): دیوار آتشین بین ماژول‌ها

در یک مخزن یکپارچه (Monorepo)، بزرگترین دشمن شما "نشتی انتزاع" (Abstraction Leak) است. اگر توسعه‌دهنده بتواند آزادانه از هر کلاسی در کلاس دیگر استفاده کند، خیلی زود با یک "کد اسپاگتی" مواجه می‌شویم.

  • اعمال قانون در کامپایلر (Compile-Time Enforcement):

    در زبان Go، مکانیزم internal packages دقیقاً برای همین هدف طراحی شده است. کدی که در پوشه internal قرار دارد، فقط توسط پکیج والد خود قابل دسترسی است و سایر ماژول‌ها نمی‌توانند آن را import کنند. این ویژگی، "Encapsulation" را از یک توصیه اخلاقی به یک اجبار کامپایلری تبدیل می‌کند.

    (در اکوسیستم‌های دیگر مثل Python/Django یا Java/Spring، این کار با استفاده از Linting Tools یا ماژول‌های جداگانه Maven/Gradle انجام می‌شود).

  • تطابق با DDD: هر ماژول در این معماری، معادل یک Bounded Context در طراحی دامنه-محور (DDD) است. ماژول Chat نباید مستقیماً مدل دیتابیسی User را ببیند؛ بلکه باید فقط با "Public API" یا "Contract" ماژول Manager صحبت کند.

۳.۲. استراتژی داده (Data Strategy): اشتراک فیزیکی، تفکیک منطقی

پاشنه آشیل اکثر مهاجرت‌های معماری، لایه دیتابیس است. در Code-Level Monolith، قانون طلایی این است:

«دیتابیس مشترک است، اما Schema خصوصی است.»

  • ممنوعیت Foreign Key: بین جداول دو ماژول مختلف (مثلاً orders و users) نباید کلید خارجی (Foreign Key) وجود داشته باشد. اگرچه این کار تضمین یکپارچگی (Referential Integrity) را سخت می‌کند، اما برای حفظ استقلال ماژول‌ها حیاتی است.

  • دسترسی غیرمستقیم: هیچ ماژولی حق ندارد مستقیماً روی جداول ماژول دیگر SELECT یا JOIN بزند. اگر ماژول Chat نیاز دارد بداند نام کاربر چیست، نباید جدول users را بخواند؛ بلکه باید متد GetUserProfile را از ماژول Manager صدا بزند. این کار باعث می‌شود اگر روزی دیتابیس ماژول Manager عوض شد، کد Chat نشکند.

۳.۳. اصل شفافیت مکان (Location Transparency)

این قلب تپنده معماری "یکپارچه مدولار" است. کدِ بیزنس لاجیک شما نباید بداند سرویسی که صدا می‌زند، در همان پروسه (In-Memory) اجرا می‌شود یا در سروری دیگر (Over Network).

  • معکوس‌سازی وابستگی (Dependency Inversion):

    به جای اینکه سرویس Order به سرویس User وابسته باشد، باید به یک Interface وابسته باشد که خودش تعریف کرده است.

    • غلط: import "myapp/user/service" و استفاده مستقیم از UserService.

    • درست: تعریف type UserProvider interface در داخل ماژول Order.

  • تصمیم‌گیری در زمان اجرا (Runtime Binding):

    این وظیفه فایل main (یا Composition Root) است که تصمیم بگیرد چه پیاده‌سازی‌ای را به این اینترفیس تزریق کند:

    1. حالت All-in-One: تزریق مستقیم آبجکت سرویس (In-Memory Adapter) -> سرعت فراخوانی: نانوثانیه.

    2. حالت Microservice: تزریق یک gRPC Client Wrapper (Network Adapter) -> سرعت فراخوانی: میلی‌ثانیه.

۳.۴. الگوی گرافانا (The Grafana Pattern): هنرِ پلی-دیپلوی

پروژه Grafana Loki (سیستم تجمیع لاگ) بهترین مثال صنعتی این معماری است. سورس کد Loki شامل کامپوننت‌های مختلفی مثل Ingester، Distributor و Querier است که همگی در یک باینری کامپایل می‌شوند.

جادو در زمان اجرا اتفاق می‌افتد:

  • اجرا با فلگ ./loki -target=all: تمام کامپوننت‌ها در یک پروسه بالا می‌آیند و با Function Call صحبت می‌کنند (مناسب برای توسعه لوکال یا بارهای کاری سبک).

  • اجرا با فلگ ./loki -target=ingester: باینری فقط نقش Ingester را بازی می‌کند و بقیه کدها خاموش می‌شوند (مناسب برای محیط‌های مقیاس‌پذیر که نیاز به ۵۰ نود Ingester داریم).

این الگو به تیم مهندسی اجازه می‌دهد معماری دیپلوی (Deployment Architecture) را بدون تغییر حتی یک خط از بیزنس لاجیک، تغییر دهند.


۴. کالبدشکافی معماری Quick Connect: تئوری در عمل

در این بخش، مستقیماً وارد "اتاق عمل" می‌شویم. پروژه اپن‌سورس Quick Connect به عنوان آزمایشگاهی برای پیاده‌سازی الگوی Code-Level Monolith در زبان Go طراحی شده است. بیایید ببینیم این تئوری‌ها چگونه به کد تبدیل شده‌اند.

۴.۱. ساختار پروژه: نظم در عین هرج‌ومرج

مخزن این پروژه یک Monorepo است، اما نه یک مخزن شلوغ و بی‌در‌وپیکر. ساختار دایرکتوری‌ها به گونه‌ای طراحی شده که "استقلال سرویس‌ها" را فریاد می‌زند:

  • app/ (قلمرو سرویس‌ها): تمام سرویس‌های اصلی (مثل chatapp، managerapp و notificationapp) در این دایرکتوری زندگی می‌کنند. نکته حیاتی اینجاست که هر سرویس دارای دیتابیس و ریپوزیتوری کاملاً ایزوله است. هیچ سرویسی حق ندارد به دایرکتوری repository سرویس دیگر سرک بکشد.

  • pkg/ (کدهای اشتراکی): از آنجا که همه کدها در یک مخزن هستند، کدهای عمومی (مثل Logger، Error Handling و ابزارهای Auth) در اینجا قرار می‌گیرند تا از دوباره‌نویسی جلوگیری شود.

  • cmd/ (نقطه شروع): اینجا جایی است که تصمیم می‌گیریم برنامه چگونه اجرا شود (میکروسرویس یا یکپارچه).

۴.۲. هنرِ تعریف Interface: قانونِ مصرف‌کننده

در Quick Connect، ارتباط مستقیم ممنوع است. اگر سرویس چت (chatapp) برای نمایش نام فرستنده پیام، به اطلاعات کاربر نیاز دارد، نباید به سرویس مدیریت کاربر (managerapp) وابسته شود.

راهکار، استفاده از Dependency Inversion است. سرویس چت یک اینترفیس تعریف می‌کند که می‌گوید: "من به کسی نیاز دارم که با گرفتن ID، نام کاربر را به من بدهد."

قانون طلایی: اینترفیس باید در محل استفاده (Consumer) تعریف شود، نه در محل ارائه (Provider).

۴.۳. الگوی آداپتور (Adapter Pattern): پل ارتباطی

اینجاست که جادوی اصلی رخ می‌دهد. برای هر وابستگی خارجی (مثل سرویس کاربر)، ما دو پیاده‌سازی (Adapter) متفاوت می‌نویسیم. تصمیم اینکه کدام استفاده شود، به زمان اجرا (Runtime) موکول می‌شود.

الف) آداپتور شبکه (gRPC Adapter):

زمانی که سیستم به صورت میکروسرویس اجرا می‌شود، این آداپتور درخواست را سریالایز کرده و روی شبکه می‌فرستد:

Go

package manager import ( "context" "github.com/syntaxfa/quick-connect/protobuf/manager/golang/userinternalpb" "google.golang.org/grpc" ) // UserInternalAdapter acts as a client adapter via gRPC network call. type UserInternalAdapter struct { client userinternalpb.UserInternalServiceClient } func NewUserInternalAdapter(conn grpc.ClientConnInterface) *UserInternalAdapter { return &UserInternalAdapter{ client: userinternalpb.NewUserInternalServiceClient(conn), } } func (ui *UserInternalAdapter) UserInfo(ctx context.Context, req *userinternalpb.UserInfoRequest, opts ...grpc.CallOption) (*userinternalpb.UserInfoResponse, error) { // Sends request over the network return ui.client.UserInfo(ctx, req, opts...) }

ب) آداپتور لوکال (In-Memory Adapter):

زمانی که سیستم به صورت Code-Level Monolith اجرا می‌شود، این آداپتور مستقیماً متد سرویس دیگر را در حافظه (RAM) صدا می‌زند. هیچ شبکه و هیچ تاخیری در کار نیست:

Go

package manager import ( "context" "github.com/syntaxfa/quick-connect/app/managerapp/service/userservice" "github.com/syntaxfa/quick-connect/protobuf/manager/golang/userinternalpb" "github.com/syntaxfa/quick-connect/types" "google.golang.org/grpc" ) // UserInternalLocalAdapter acts as a client local adapter with func calls. type UserInternalLocalAdapter struct { userSvc *userservice.Service // Direct reference to the service struct } func NewUserInternalLocalAdapter(userSvc *userservice.Service) *UserInternalLocalAdapter { return &UserInternalLocalAdapter{ userSvc: userSvc, } } func (uil *UserInternalLocalAdapter) UserInfo(ctx context.Context, req *userinternalpb.UserInfoRequest, _ ...grpc.CallOption) ( *userinternalpb.UserInfoResponse, error) { // Direct Function Call (No Network) resp, sErr := uil.userSvc.UserInfo(ctx, types.ID(req.GetUserId())) if sErr != nil { return nil, sErr } // Convert Domain Entity back to Protobuf to satisfy the contract return convertUserInfoToPB(resp), nil }

یک تصمیم معماری هوشمندانه: Protobuf به عنوان قرارداد واحد

نکته ظریفی در کد بالا وجود دارد: ورودی و خروجی هر دو آداپتور، Protobuf است.

ما در Quick Connect پذیرفتیم که Protobuf نقش Contract نهایی را بازی کند. حتی در حالت لوکال، داده‌های دامین (types.ID) به فرمت پروتوباف تبدیل می‌شوند. این کار باعث می‌شود سویچ کردن بین حالت لوکال و شبکه کاملاً شفاف باشد و نیازی به تغییر لایه سرویسِ فراخواننده نباشد.

۴.۴. نقطه اوج: باینری All-in-One

تمام این قطعات پازل در فایل cmd/all-in-one/main.go کنار هم قرار می‌گیرند.

این فایل، نقش Composition Root را بازی می‌کند. در اینجا:

  1. تمام سرویس‌ها (Chat, Manager, Notification) در حافظه بالا می‌آیند (Instantiate می‌شوند).

  2. به جای اینکه کلاینت‌های gRPC به سرویس‌ها تزریق شوند، LocalAdapterها ساخته شده و به سرویس‌ها پاس داده می‌شوند.

  3. نتیجه نهایی یک فایل اجرایی واحد است که تمام قابلیت‌های سیستم را دارد، اما ارتباطات بین اجزای آن با سرعت Function Call انجام می‌شود.

۴.۵. چرا Quick Connect این مسیر را انتخاب کرد؟

پاسخ در یک جمله خلاصه می‌شود: واقع‌گرایی.

بیش از ۹۰٪ پروژه‌های نرم‌افزاری، هرگز به مقیاس بزرگی نمی‌رسند. شروع کردن یک پروژه با ۱۰ میکروسرویس، ۵ دیتابیس و کلاسترهای کوبرنتیز، تنها باعث هدررفت منابع و پیچیدگی بیهوده می‌شود.

معماری Quick Connect به شما اجازه می‌دهد:

  1. امروز با سادگیِ یک all-in-one محصول را توسعه دهید و دیپلوی کنید (Easy Deployment).

  2. فردا اگر بخشی از سیستم (مثلاً Chat) زیر بار ترافیک رفت، فقط با تغییر فایل main، آن را جدا کرده و به یک میکروسرویس مستقل تبدیل کنید (Scalability without Rewrite).

این یعنی معماری استقرار (Deployment) از معماری کد (Code) جدا شده است؛ و این همان آزادی عملی است که هر مهندس نرم‌افزاری آرزوی آن را دارد.


۵. بازگشت سرعت و سادگی

شاید بزرگترین قربانی معماری میکروسرویس، "تجربه توسعه‌دهنده" (Developer Experience - DX) باشد. در یک ستاپ میکروسرویس معمولی، یک برنامه‌نویس برای اینکه بتواند یک فیچر ساده را تست کند، باید ۱۰ کانتینر داکر را بالا بیاورد، پورت‌ها را مدیریت کند و با مصرف رم ۳۲ گیگابایتی سیستم خود بجنگد.

اما در Quick Connect، داستان متفاوت است.

۵.۱. پایانِ جهنمِ Localhost

در معماری Code-Level Monolith، محیط توسعه شما دقیقاً شبیه محیط پروداکشن نیست؛ و این خوب است!

برای توسعه‌دهنده‌ای که روی سرویس Chat کار می‌کند، مهم نیست که سرویس Auth در یک پاد کوبرنتیز جداگانه اجرا می‌شود یا نه. او فقط می‌خواهد کدش کار کند.

در این پروژه، کل سیستم با یک دستور ساده بالا می‌آید:

Bash

go run cmd/all-in-one/main.go

این دستور جادویی، تمام سرویس‌ها (Manager, Chat, Notification) را در یک پروسه واحد اجرا می‌کند.

  • مصرف منابع: کمتر از ۱۰۰ مگابایت رم (در مقایسه با چندین گیگابایت برای میکروسرویس‌ها).

  • Hot Reload: تغییر در کد و ریستارت کردن سرور کمتر از ۱ ثانیه طول می‌کشد.

  • دیباگینگ: گذاشتن یک Breakpoint در سرویس Chat و دنبال کردن (Step Into) کد تا سرویس Manager، بدون هیچ پیچیدگیِ Remote Debugging امکان‌پذیر است.

۵.۲. تست‌های یکپارچه (Integration Tests): سریع، پایدار، ارزان

تست‌های End-to-End در دنیای میکروسرویس‌ها معمولاً کند و شکننده (Flaky) هستند، چون به شبکه و بالا بودن تمام سرویس‌ها وابسته‌اند.

در رویکرد Quick Connect، ما می‌توانیم تست‌های یکپارچه را در حافظه اجرا کنیم.

از آنجا که ماژول‌ها از طریق اینترفیس و LocalAdapter به هم وصل شده‌اند، می‌توانیم سناریویی بنویسیم که:

۱. یک کاربر در Manager ثبت‌نام کند.

۲. با همان توکن در Chat پیام بفرستد.

۳. و در Notification اعلان دریافت کند.

همه این‌ها در کسرری از ثانیه و بدون رد شدن حتی یک بایت از کارت شبکه انجام می‌شود. این یعنی بازخورد سریع به توسعه‌دهنده و CI Pipeline‌هایی که به جای ۲۰ دقیقه، در ۲ دقیقه پاس می‌شوند.

۵.۳. حذف "تاخیر" در ماژول‌های پرحرف (Chatty Modules)

در سیستم‌های چت، برخی سرویس‌ها ذاتاً "پرحرف" هستند. مثلاً سرویس WebSocket ممکن است برای هر پیامی که می‌آید، نیاز داشته باشد به سرویس manager درخواستی بزند.

اگر این درخواست ها نیاز به یک فراخوانی gRPC داشته باشد (۲ میلی‌ثانیه)، و شما ۱۰,۰۰۰ پیام در ثانیه داشته باشید، سربار شبکه سیستم را خفه می‌کند.

در حالت All-in-One، این چک کردن تبدیل به یک Function Call می‌شود که در حد نانوثانیه زمان می‌برد. این یعنی ما بدون هیچ بهینه‌سازی پیچیده‌ای، پرفورمنس را چندین برابر کرده‌ایم.


۶. آینده معماری

کاری که ما در Quick Connect به صورت دستی (Explicit) انجام دادیم—یعنی تعریف اینترفیس‌ها و نوشتن دو نسخه آداپتور (Local vs Remote)—نمایشی از آینده‌ای است که مهندسی نرم‌افزار به سمت آن حرکت می‌کند.

۶.۱. ظهور فریم‌ورک‌های Poly-Deployment (Google Service Weaver)

شرکت گوگل اخیراً فریم‌ورکی به نام Service Weaver را برای زبان Go معرفی کرده است که دقیقاً همین فلسفه را دنبال می‌کند، اما با یک تفاوت: "خودکارسازی جادویی".

در Quick Connect، ما خودمان تصمیم می‌گیریم که UserLocalAdapter استفاده شود یا UserGrpcAdapter. اما در Service Weaver:

۱. شما کد را طوری می‌نویسید که انگار همه چیز یکپارچه (Monolith) است.

۲. کامپوننت‌ها را با اینترفیس‌های معمولی Go تعریف می‌کنید.

۳. در زمان Deploy، با یک فایل کانفیگ ساده (weaver.toml) به سیستم می‌گویید: "این کامپوننت و آن کامپوننت را جدا کن و در سرورهای مختلف اجرا کن."

خودِ فریم‌ورک Service Weaver کد را اسکن می‌کند و اگر تشخیص دهد دو کامپوننت در یک پروسه هستند، از Function Call استفاده می‌کند (با Serialize کردن صفر) و اگر جدا باشند، خودش کد gRPC و Protobuf را تولید و اجرا می‌کند.

این یعنی:

«جدایی کامل معماری منطقی (Logical Architecture) از معماری فیزیکی (Physical Architecture).»


۷. توصیه نهایی: استراتژی برنده برای ۹۹٪ پروژه‌ها

و در نهایت چیزی که می خواهم بگویم، این است:

«با Monolith شروع کنید، اما طوری کد بزنید که انگار میکروسرویس است.»

تاریخچه مهندسی نرم‌افزار قبرستانی پر از استارتاپ‌هایی است که زیر بارِ پیچیدگیِ مدیریتِ ۵۰ میکروسرویس، قبل از اینکه به ۱۰۰۰ کاربر اول برسند، دفن شده‌اند. و همچنین شرکت‌هایی که با "توپ بزرگ گِل" خفه شده‌اند.

نقشه راه پیشنهادی ما (بر اساس تجربه Quick Connect):

  1. روز اول (شروع پروژه):

    • ساختار Monorepo ایجاد کنید.

    • از پوشه‌های internal در Go استفاده کنید تا مرزها را قفل کنید(که البته ما استفاده نکردیم‌:)).

    • ارتباط بین ماژول‌ها را فقط و فقط از طریق Interface برقرار کنید.

    • سیستم را به صورت All-in-One مستقر کنید (مثل cmd/all-in-one/main.go).

  2. روز صدم (رشد ترافیک):

    • چون کدهایتان ایزوله است، هیچ بدهی فنی‌ای ندارید.

    • همچنان روی یک سرور قوی‌تر (Vertical Scaling) دیپلوی کنید. (هزینه سرور قوی، ارزان‌تر از هزینه تیم DevOps است).

  3. روز هزارم (مقیاس‌پذیری عظیم):

    • پروفایلر (pprof) نشان می‌دهد که ماژول Chat گلوگاه شده است.

    • فقط برای ماژول Chat، یک main.go جداگانه می‌نویسید (مثل cmd/chat/main.go).

    • آداپتور Local را با آداپتور gRPC جایگزین می‌کنید.

    • حالا شما یک سیستم میکروسرویس دارید، دقیقاً در جایی که لازم داشتید و دقیقاً در زمانی که لازم داشتید.

Code-Level Monolith یک عقب‌گرد نیست؛ بلکه بلوغ مهندسی است. این معماری به شما اجازه می‌دهد سرعت یک استارتاپ و نظم یک اینترپرایز را همزمان داشته باشید.

اگر از این مقاله لذت بردید، ممنون میشم از کوئیک کانکت حمایت کنید و بهمون استار بدید و با بقیه به اشتراک بذارید:
https://github.com/syntaxfa/quick-connect

لینک مقاله در dev.to:
https://dev.to/alireza_feizi_2aa9c86cac4/code-level-monolith-the-hybrid-architecture-the-art-of-flexible-deployment-2jm2

#code_level_monolith #go #modulith

معماریgolangdeploy
۹
۳
علیرضا فیضی
علیرضا فیضی
توسعه دهنده بک‌اند / پایتون / گولنگ
شاید از این پست‌ها خوشتان بیاید