Hoivalani — مراقبت سلامت در منزل
مدیر ارشد فناوری یک محصول مراقبت در منزل: event storming، تیمی از ۴ برنامهنویس، ۳ طراح و ۲ پرستار، میکروسرویسهای Go با DDD و معماری ششضلعی روی RabbitMQ و CQRS، و پنل پرستاران با ردیابی زنده.
- اندازهی تیم
- ۹
- مدت
- تاریخ تقریبی۱۵ ماه
- کلاینت API دستنویس
- ۰
زمینه
Hoitek یک شرکت فنلاندی است که Hoivalani را میسازد؛ محصولی برای مراقبت سلامت در منزل و خانواده: پرستارها به خانهی بیماران میروند و شرکت باید این ویزیتها را زمانبندی کند، بداند هر پرستار کجاست و پروندهها را مرتب نگه دارد. اوایل ۲۰۲۲ [verify] بهعنوان مدیر ارشد فناوری (CTO) به تیم پیوستم؛ دورکار از تهران و زیر نظر بنیانگذار [verify]، با این مأموریت که محصول را از [ایده / نمونهی اولیه — verify] به یک پلتفرم کارا برسانم و تیمی بسازم که آن را اداره کند.
وضعیت. وقتی رسیدم [بکاندی وجود نداشت / یک نمونهی اولیه بود — verify]، تیم طراحی به جهتدهی نیاز داشت و هیچکدام از مهندسها در این دامنه زندگی نکرده بودند. بزرگترین محدودیت دانش دامنه بود: زمانبندی در حوزهی سلامت قاعدههایی دارد که هیچکس جایی ننوشته است. محدودیت دوم این بود که تیمی کوچک و دورکار میان [فنلاند و ایران — verify] بودیم و فرایندی لازم داشتیم که به حضور در یک اتاق وابسته نباشد.
چه کردم
- پرستارها را واقعاً وارد اتاق کردم. جلسههای ایونت استورمینگ را روی Miro برگزار کردم و دو پرستار عضو تیم بودند، نه ذینفعانی که یک بار با آنها مصاحبه کنیم. پیش از نوشتن حتی یک خط کد، چرخهی عمر ویزیت را ترسیم کردیم: درخواست، برنامهریزی، رفتن، رسیدن، درمان، گزارش، صورتحساب [verify]. گزینهی کنارگذاشته: نوشتن یوزر استوری از روی توضیح بنیانگذار؛ سریعتر بود و اشتباه.
- Go را با طراحی دامنهمحور (DDD) و معماری ششضلعی انتخاب کردم. دامنه پر از قاعده بود و با یادگیری ما تغییر میکرد؛ معماری ششضلعی قاعدهها را از لایهی انتقال و ذخیرهسازی جدا نگه داشت. سرویسها از طریق RabbitMQ با هم حرف میزدند، با لایهی انتقالی که اجازه میداد بین همگام و ناهمگام جابهجا شویم بیآنکه به دامنه دست بزنیم. گزینهی کنارگذاشته: یک مونولیت Node؛ شروعش سریعتر بود، اما دیده بودم در [Lemon / کار قبلی — verify] وقتی دامنه غنی شد، نگهداریاش ناممکن شد.
- CQRS و Redis برای سمت خواندن. برنامهها و وضعیت زندهی پرستارها به ازای هر نوشتن صدها بار خوانده میشوند، پس کوئریها از مدلهای خواندنیِ مبتنی بر Redis میگذشتند. هزینه: سازگاری نهایی مدتی پنل را سردرگم کرد (بازنگری را ببینید).
- تولید کد OpenAPI → React Query را نوشتم. کد بکاند مشخصات OpenAPI میساخت و یک ابزار آنها را به هوکهای تایپشدهی React Query تبدیل میکرد. فرانتاند هیچوقت فراخوانی API را دستی ننوشت. این همان تکهای است که از آن به بعد به هر پروژهای بردهام.
- Docker برای همهی محیطها و CI/CD و رجیستری GitLab روی سرور خودمان. توسعه، تست و پروداکشن همه از ایمیجهای یکسان اجرا میشدند. گزینهی کنارگذاشته: سرویس ابری GitLab.com — [محل نگهداری داده / هزینه — verify].
- پنل مدیریت پرستاران را خودم ساختم (React، RTK، MUI، i18next): چرخهی کارکنان و شیفتها، موقعیت زندهی پرستاران با WebSocket، نمودارها و نقشهی Leaflet برای اعزام.
- طراحها را در همهی صفحههای Figma هدایت کردم تا طراحی همیشه یک قدم جلوتر از مهندسی بماند.
نتایج (میزان اطمینان در پرانتز)
- پلتفرم در پروداکشن با [N] پرستار و [N] بیمار در [شهر/منطقه] (verify)
- جذب و آنبوردینگ تیم: ۴ برنامهنویس، ۳ طراح، ۲ پرستار (دقیق، رزومه)
- [N] سرویس روی RabbitMQ و [N] انتشار در هفته از طریق CI (verify)
- پس از عرضهی تولید کد، هیچ کلاینت API دستنویسی در فرانتاند نماند (دقیق، به حکم ساختار)
- موقعیت و وضعیت زندهی هر ویزیت فعال در پنل (دقیق، رزومه)
- [آپتایم / تعداد رخدادها / هر عددی که بنیانگذار تأیید کند — verify]
- اندازهی تیم
- ۹
- مدت
- تاریخ تقریبی۱۵ ماه
- کلاینت API دستنویس
- ۰
آنچه درست پیش رفت
- غرقشدن در دامنه هزینهاش را جبران کرد. حضور پرستارها در تیم دستکم [سه — verify، پیشنویس] قابلیت را پیش از ساختهشدن حذف کرد. همان درسی که با پختن پنجاه غذا در CookThePot گرفته بودم.
- تولید کد ریتم تیم را عوض کرد. بکاند یک spec را مرج میکرد و فرانتاند چند دقیقه بعد تایپها را داشت؛ باگهای یکپارچهسازی [تقریباً به صفر رسید — verify، پیشنویس].
- معماری ششضلعی دوام آورد. وقتی [قاعدههای زمانبندی تغییر کرد — verify، پیشنویس]، تغییر فقط در پکیج دامنه بود.
آنچه اشتباه پیش رفت
روند
[پیشنویس — فرضیه: نگه دارید، اصلاح یا جایگزین کنید] [ماه سوم — verify]: پنل پس از نوشتنها وضعیت کهنهی پرستاران را نشان میداد. یک پرستار در یک دمو متوجه شد، نه مانیتورینگ. علت ریشهای در [دو روز — verify] روشن شد و رفعش [یک هفته — verify] طول کشید.
علتها
- CQRS پیش از آنکه تیم درک مشترکی از سازگاری نهایی داشته باشد معرفی شد؛ پنل فرض میکرد هر خواندن بلافاصله پس از نوشتن بهروز است.
- باطلسازی مدلهای خواندنی در سه جا پخش بود و هیچ مالک واحدی نداشت.
- هیچ تست سرتاسری نداشتیم که از سمت نوشتن تا سمت خواندن برود؛ پایپلاین CI هر سرویس را جداگانه تست میکرد.
- بازبینیهای دورکار در منطقههای زمانی مختلف یعنی PRهای باطلسازی را هر کسی که بیدار بود مرج میکرد.
اثر
[یک — verify] دمو بد پیش رفت؛ اعتماد پرستارها به برچسب «زنده» کمی آسیب دید؛ حدود [یک اسپرینت — verify] صرف رفع مشکل و توضیح دوبارهی مدل شد.
اقدامها
- باطلسازی مدلهای خواندنی به یک ماژول با یک مالک منتقل شد✓ انجام شد
- مجموعهای از تستهای یکپارچهسازی مسیر RabbitMQ را در CI اجرا کرد✓ انجام شد
- پنل بهجای وانمود به همگامبودن، وضعیت «در حال بهروزرسانی…» را نشان داد✓ انجام شد
- CQRS در یک ADR مستند شد که کل تیم بازبینی کرد✓ انجام شد
مورد دوم بازنگری [نامزد — verify یا حذف]: ابزار OpenAPI پیش از پایدارشدن API ساخته شد، پس تغییرات اولیهی spec ابزار را هم وادار به تغییر میکرد؛ در نگاه به عقب، ابزار را وقتی بساز که دو سرویس قرارداد پایدار دارند.
اگر برمیگشتم
- نگهدارباز هم کار را با ایونت استورمینگ شروع میکردم
- نگهدارباز هم Go و معماری ششضلعی را انتخاب میکردم
- تغییر بدهCQRS را هر بار برای یک aggregate معرفی میکردم، با تیمی که اولی را از نزدیک ببیند، نه بهعنوان تصمیم معماری روز اول
- تغییر بدهمسیر تست سرتاسری را پیش از ساخت سرویس دوم راه میانداختم