موصى بهقيد الإنجاز

HealthPack

مشروع شخصي — مهندس منفرد · September 2026 — Present

  • Java 25
  • Spring Boot 4
  • REST
  • GraphQL
  • gRPC
  • WebSocket
  • Apache Kafka
  • Kafka Streams
  • RabbitMQ
  • PostgreSQL
  • pgvector
  • Redis
  • MinIO / S3
  • Keycloak (OIDC)
  • Spring AI
  • Claude (Anthropic API)
  • HAPI FHIR R4
  • HL7 v2
  • Docker
  • Kubernetes
  • OpenTelemetry
  • Stripe

منصة لتنسيق العمل داخل المستشفى أعمل على بنائها حاليًا: ملفات المرضى، والمواعيد، والاستشارات السريرية، ونتائج المختبر، والوصفات الطبية، والوثائق، ومساعد بالذكاء الاصطناعي، صُممت كخدمات مستقلة تبقى متزامنة بالإعلان عمّا حدث بدل أن يسأل بعضها بعضًا. اكتمل التصميم عبر المخططات الاثني عشر أدناه، والبناء جارٍ: خدمات المرضى والمواعيد والبوابة تعمل بالفعل، والخدمة السريرية هي التالية.

المشكلة الأساسية

معلومات المريض موزعة على أجزاء. مواعيده في نظام، وتحاليله في آخر، ووصفاته في ثالث، وفواتيره في رابع. ولا يتواصل أي منها مع الآخر كما ينبغي.

والعواقب عادية ومتكررة. يصف طبيب دواءً دون أن يرى أنه يتعارض مع دواء وصفه طبيب آخر الشهر الماضي. تبقى نتيجة حرجة دون أن يقرأها أحد لساعات. يتصل مريض بالاستقبال ليسأل عن أمر تجيب عنه وثيقة لا يجدها أحد. ويعيد الموظفون كتابة المعلومات نفسها في أربع شاشات مختلفة.

لا شيء من هذا مشكلة تقنية بالمعنى المثير. إنها مشكلة تنسيق. المعلومة موجودة، لكنها لا تصل أبدًا إلى حيث يُحتاج إليها، في الوقت الذي يُحتاج إليها فيه.

الحل

صُمم HealthPack من خمس عشرة خدمة صغيرة مستقلة بدل خدمة واحدة ضخمة. تتولى كل منها مهمة واحدة وتتقنها: واحدة لملفات المرضى، وواحدة للمواعيد، وواحدة لنتائج المختبر، وواحدة للوصفات، وواحدة للوثائق، وواحدة للمساعد الذكي.

وتبقى متزامنة بأن يبث بعضها لبعض ما يحدث. حين تُعتمد نتيجة مخبرية، تعلن خدمة المختبر ذلك مرة واحدة على lab.result.finalized. وكل ما يعنيه الأمر — السجل الطبي الزمني للمريض، ونظام تنبيهات الطبيب، ومسار الفوترة، وفهرس البحث الخاص بالمساعد الذكي — يتلقى الإعلان نفسه عبر Kafka ويتفاعل معه باستقلالية. لا أحد مضطر لأن يتذكر إبلاغ أحد.

هذه هي الفكرة كلها: النظام يخبر نفسه بما حدث، والأمور الصحيحة تحدث تلقائيًا. ينقل Kafka الوقائع — أشياء حدثت، قابلة لإعادة التشغيل، ومحفوظة — وينقل RabbitMQ المهام — أنشئ هذا الملف PDF، أرسل هذا الإشعار — عامل واحد، ومحاولة واحدة، تُعاد عند الفشل.

الأجزاء الصعبة

الأعطال تحدث، وعلى النظام أن يصمد أمامها. في نظام من خمس عشرة خدمة مستقلة، توقف إحداها مؤقتًا أمر عادي لا استثنائي. ويحصر تصميم HealthPack العطل في مكانه: إذا توقفت خدمة الإشعارات، تستمر حجوزات المواعيد، وتنتظر التذكيرات في RabbitMQ ثم تُرسل حين تعود الخدمة. لا شيء يضيع.

لا ينبغي أن يحدث شيء مرتين. يجب ألا تصدر وصفة طبية مرتين لأن طلبًا عبر الشبكة أُعيدت محاولته. كل عملية إنشاء وثيقة تحمل مفتاح عدم تكرار يُتحقق منه قبل بدء أي عمل، بحيث تعطي إعادة الأمر نفسه النتيجة ذاتها التي يعطيها تنفيذه مرة واحدة.

السرعة حيث تهم. تجمع لوحة تحكم الطبيب معلومات من خمس خدمات مختلفة. لو بُنيت بسذاجة لكانت عشرات الطلبات المنفصلة وصفحة بطيئة. تجمعها طبقة GraphQL في استدعاء gRPC واحد لكل خدمة بدل استدعاء لكل صف مريض، فتُحمَّل الصفحة في رحلة ذهاب وإياب واحدة.

الخصوصية قيد في التصميم لا ميزة إضافية. يُتحقق من الموافقة قبل أن يقرأ المساعد الذكي أي شيء. وتُحذف البيانات الشخصية قبل أن تغادر أي معلومة خوادم المستشفى نفسه. ليست هذه إضافات — فالبنية تفترضها منذ المخطط الأول.

ملفات المرضى

هوية واحدة موثقة لكل مريض، حتى تتحدث كل الخدمات الأخرى عن الشخص نفسه. تُشفَّر الحقول الحساسة كلٌّ على حدة، لا مجرد القول إن «قاعدة البيانات مشفرة».

المواعيد

يحجز المرضى عبر الإنترنت ويرون التوفر الحقيقي. ويجعل قيد استبعاد في قاعدة البيانات حفظ حجزين للطبيب نفسه في الفترة نفسها مستحيلًا بنيويًا، لا مجرد أمر يُتحقق منه بعناية.

السجلات السريرية

يسجل الأطباء الزيارات والتشخيصات والقياسات. كل شيء مؤرخ ومربوط بالسجل الزمني للمريض ومرمَّز وفق ICD-10 وSNOMED CT وLOINC، وهي المعايير التي تستعملها المستشفيات فعلًا.

نتائج المختبر

تصل النتائج بصيغة المراسلة الاستشفائية القياسية (HL7 v2) مباشرة من أجهزة المختبر. وتراقب عملية بث مستمر كل نتيجة بحثًا عن القيم الخطيرة وتنبه الطبيب في ثوانٍ، متجاوزة وضع عدم الإزعاج.

الوصفات الطبية

قبل إصدار أي وصفة، تُقارن بكل ما يتناوله المريض حاليًا. التداخل الدوائي الخطير يوقف الطلب ويبيّن السبب بدقة؛ وأي تجاوز يُسجَّل بشكل دائم مع السبب الذي يقدمه الطبيب.

الوثائق

تُنشأ تقارير الخروج وتقارير المختبر والوصفات والفواتير بصيغة PDF صالحة للأرشفة، موقعة تشفيريًا بحيث يُكشف أي تلاعب. ويجري الإنشاء في الخلفية، فلا أحد ينتظر أمام شاشة تحميل.

المساعد الذكي

يطرح المرضى والطاقم أسئلتهم بلغة عادية. ولا يجيب المساعد إلا انطلاقًا من الوثائق الفعلية لذلك المريض، ويذكر مصدر كل معلومة، ويصرّح بوضوح حين لا تتضمن الوثائق جوابًا.

المراسلة

محادثة فورية بين المرضى والأطباء، ومع المساعد الذكي. تصل الإجابات رمزًا برمز بدل أن تظهر بعد انتظار طويل.

الإشعارات

تذكيرات بالمواعيد، وتنبيهات بجاهزية النتائج، وقيم حرجة، تُرسل عبر إشعارات الهاتف أو البريد الإلكتروني أو الرسائل القصيرة حسب ما اختاره كل شخص. وتسلك التنبيهات الحرجة مسارًا منفصلًا وأسرع يتجاوز ساعات الهدوء.

الفوترة

تُنشأ الفواتير تلقائيًا عند انتهاء الزيارة، مع معالجة الدفع بالبطاقة عبر Stripe، ونمط outbox معاملاتي يُبقي الطرفين متزامنين.

سجل التدقيق

كل إجراء يقوم به أي شخص يُسجَّل في سجل مسلسل بالتجزئة يكشف أي تلاعب. تعديل أي قيد سابق يكسر السلسلة ويُكتشف — وهذا ما تفرضه القوانين، وتطبقه أغلب الأنظمة بشكل سيئ.

البنية المعمارية في مخططات

اثنا عشر مخططًا، بالترتيب الذي أشرح به النظام فعلًا لمهندس آخر: الصورة الكاملة أولًا، ثم البيانات التي تملكها كل خدمة، ثم ما تعرضه كل خدمة، ثم ستة مسارات طلبات متتبَّعة من بدايتها إلى نهايتها.

01

البنية العامة — التدفق الكامل للنظام

كل خدمة، وكل بروتوكول، وكل وسيط، في صفحة واحدة.

هذا هو المخطط الذي أفتحه أولًا. لا يتواصل العملاء إلا مع edge-gateway، الذي يتحقق من كل JWT عبر Keycloak قبل تمرير الطلب: تذهب استدعاءات REST مباشرة إلى خدمة النطاق، ويذهب GraphQL إلى BFF، وتذهب ترقية WebSocket إلى chat-service. لا شيء في الخلف يعيد التحقق من الهوية؛ البوابة هي المكان الوحيد الذي يفعل ذلك، وكل ما عداها يثق في البيانات التي تمررها.

الأسهم المزدوجة السميكة هي gRPC، وهي تتجمع حول terminology-service لسبب وجيه: فهي تُستدعى في كل كتابة سريرية تقريبًا (التحقق من رمز ICD-10، وفحص تداخل دوائي، وتحديد رمز LOINC)، لذا هي المكان الوحيد الذي سيُحس فيه ببطء البروتوكول في كل مكان. الأسهم المنقطة هي Kafka و RabbitMQ، والمخطط في حقيقته حجة للإبقاء عليهما منفصلين: يوزع Kafka حدثًا واحدًا على خمسة مستهلكين مختلفين (فـ lab.result.finalized وحده يغذي الجانب السريري والفوترة والمساعد الذكي واشتراكات GraphQL)، بينما يحمل RabbitMQ الأوامر إلى عامل واحد بالضبط — ملف PDF يُنشأ، أو إشعار يُرسل.

اقرأه من الأعلى إلى الأسفل وستحكي الطبقات قصتها بنفسها: حافة لا تفعل سوى التحقق والتوجيه، وطبقة تركيب لا توجد إلا لتوفير رحلات الذهاب والإياب على الواجهة الأمامية، وسبع خدمات نطاق تملك كل منها قاعدة Postgres واحدة ولا شيء غيرها، وخدمة gRPC داخلية واحدة خلفها جميعًا، وثلاثة عمال غير متزامنين لا يستدعيهم أي عميل مباشرة.

02

مخطط الأصناف — الهوية والنواة السريرية

Patient و Practitioner و Consent و Appointment و Encounter — الكيانات التي يتفرع عنها كل ما سواها.

كل شيء في HealthPack يعود في النهاية إلى Patient، لذا كان هذا أول نموذج صممته. العلاقة التي تستحق التوقف عندها هي Appointment "1" --> "0..1" Encounter : produces: الموعد حجز، واللقاء هو الزيارة الفعلية، وهما سجلان منفصلان عن قصد. قد يحجز مريض موعدًا ولا يحضر؛ وقد ينتج عن زيارة بلا موعد لقاءٌ لا يقف خلفه أي موعد. ودمجهما كان سيجعل تمثيل الغياب والزيارات دون موعد أمرًا مستحيلًا بشكل نظيف.

يتفرع Consent مباشرة من Patient بدل أن يُدفن في جدول إعدادات، لأن المساعد الذكي يتحقق منه في المسار الحرج لكل سؤال يجيب عنه — إذ يُستدعى isActiveFor(ConsentScope) قبل استرجاع أي وثيقة، لا بعده. ويملك Encounter كلًّا من Observation وCondition عبر التركيب (المعيّن الممتلئ)، أي أنهما لا يمكن أن يبقيا بعد الزيارة التي سُجّلا فيها — فالقياس له دائمًا تاريخ وسياق سريري، ولا يطفو منفصلًا أبدًا.

03

مخطط الأصناف — الطلبات والوثائق والفوترة

كيف ينتهي طلب مختبر ووصفة طبية وفاتورة إلى إنتاج النوع نفسه من المخرجات.

الأسهم المتقطعة الثلاثة التي تلتقي عند DocumentJob هي جوهر هذا المخطط: LabOrder وPrescription وInvoice مفاهيم سريرية أو مالية لا علاقة بينها، لكنها تنهي حياتها بالطريقة نفسها — بإنشاء ملف PDF. ونمذجة هذا الالتقاء صراحة سمحت بكتابة document-service مرة واحدة وبشكل عام، بدل ثلاث مرات بثلاثة مسارات PDF مختلفة بفوارق دقيقة.

يحمل DocumentJob مفتاح idempotencyKey لسبب ملموس: قد يتلقى مستهلك RabbitMQ الرسالة نفسها مرتين (هذا هو الاتفاق الذي يقدمه RabbitMQ — التسليم مرة واحدة على الأقل، لا مرة واحدة على الأكثر)، ويجب ألا تُنشأ وصفة الطبيب وتُوقَّع وتُحفظ مرتين بسبب استدعاء شبكي أُعيدت محاولته. ويوجد عداد attempts والدالة markFailed(String) في المهمة نفسها كي تظهر أي عملية إنشاء عالقة كبيانات، لا كفراغ صامت.

04

مخطط الأصناف — الذكاء الاصطناعي والمحادثة والإشعارات والتدقيق

كيف تُبنى فعلًا إجابة RAG ورسالة محادثة وسجل تدقيق.

لا يحفظ RagQuery الإجابة فقط، بل يحتفظ بـ inputTokens وcacheReadTokens إلى جانب List~Citation~، لأن إجابة RAG بلا مصدر ليست معلومة تستحق أن تُعرض على طبيب، وعدد الرموز دون رقم القراءة من الذاكرة المؤقتة لا يخبرك شيئًا عمّا إذا كان التخزين المؤقت للتعليمات يعمل فعلًا في بيئة الإنتاج. ويحمل كل Citation نطاق صفحات، لا معرّف وثيقة فحسب، حتى يتمكن المساعد من الإشارة إلى الفقرة التي يقتبس منها بالضبط.

AuditRecord هو الصنف الوحيد في هذا المخطط الذي يشير إلى نفسه: AuditRecord --> AuditRecord : prevHash chain. تُحسب قيمة hash لكل سجل من حقوله الخاصة مضافًا إليها تجزئة السجل السابق، لذا فإن تعديل أي قيد سابق — ولو حقلًا واحدًا — يكسر كل التجزئات التي تليه. والدالة verifyChain(AuditRecord prev) هي ما يستدعيه المدقق فعلًا، وهي إما تؤكد السلسلة أو تخبرك بالضبط أين انكسرت.

05

دوال الخدمات — واجهة الاستدعاءات المتزامنة

كل دوال REST و gRPC التي تعرضها الخدمات الثماني المدفوعة بالطلبات، في عرض واحد.

هذا أقرب إلى خريطة لمن يستدعي من بشكل متزامن منه إلى مخطط أصناف. وأسهم الاعتماد الثلاثة ..> المتجهة إلى TerminologyService من ClinicalService وPharmacyService والشيفرة القريبة من SchedulingService تؤكد قرار التصميم الذي ألمح إليه المخطط العام: استعلامات المصطلحات هي الاستدعاء المتزامن الوحيد العابر لكل الخدمات حقًا، ولهذا بالضبط تعتمد تلك الخدمة على gRPC وحده لا على REST.

يستحق BffGraphQlResolvers قراءة متأنية: فـ batchLoadPatients(Set~UUID~) : CompletableFuture~Map~ هي دالة التجميع في DataLoader، وتوقيعها هو السبب الكامل في تجنب طبقة GraphQL لاستعلامات N+1 — إذ تطلب المحللات مريضًا واحدًا في كل مرة، ويجمع DataLoader المعرّفات خلال دورة واحدة من حلقة الأحداث، ثم تجلبها هذه الدالة كلها في استدعاء gRPC واحد. وتُرجع onVitalsUpdated وonLabResultReady قيمة Flux لا قيمة مفردة — فهي اشتراكات GraphQL يغذيها مستهلكو Kafka في الخلفية، لا الاستطلاع المتكرر.

06

دوال الخدمات — الخدمات غير المتزامنة والفورية

الخدمات التي لا يستدعيها شيء مباشرة — بل تتفاعل فقط مع طابور أو تدفق.

لاحظ ما يغيب عن كل دالة هنا: لا توجد أي createX أو getX معروضة للعميل. DocumentService.onRenderCommand، وNotificationService.onNotifyCommand وonCriticalValue، وAiRagService.onDocumentTextExtracted — كل نقطة دخول تبدأ بـ on، لأن كل نقطة دخول معالج رسائل لا نقطة نهاية. يستطيع العميل الاستعلام عن حالة مهمة عبر واجهة REST صغيرة، لكنه لا يستطيع أبدًا أن يطلب من هذه الخدمات فعل شيء مباشرة؛ كل ما يمكنه هو أن يطلب ذلك من طابور.

يمثل ChatService ..> AiRagService : gRPC server-stream الاستدعاء الوحيد الذي يبدو متزامنًا في مخطط غير متزامن في مجمله، وهو موجود لأن رد المحادثة يجب أن يبدو فوريًا — فتمريره عبر Kafka سيضيف قفزة طابور سيلاحظها المستخدم وهو ينتظر ظهور الرموز. أما AiRagService.evaluateGoldenSet() فدالة بلا مستدعٍ ظاهر هنا عن قصد: إذ تستدعيها منظومة CI لا النظام أثناء تشغيله، لتقييم جودة الإجابات مقابل مجموعة ثابتة من أزواج الأسئلة والأجوبة قبل نشر أي تغيير.

07

تسلسل — قراءة Patient 360

تحميل واحد للوحة التحكم، وخمس خدمات، واستدعاء gRPC واحد لكل منها.

هذا هو التسلسل الذي يبرر الجمع بين GraphQL و gRPC برمته. يفتح الطبيب لوحة تحكم تحتاج البيانات الشخصية للمرضى ولقاءاتهم ونتائج تحاليلهم — ثلاث خدمات، وربما عشرات المرضى على الشاشة في آن واحد. الخطوة 10، «DataLoader collects all IDs in one event-loop tick»، هي اللحظة الحاسمة: بدل نمط N+1 الساذج (استدعاء لكل مريض ولكل حقل)، يُوضع طلب كل محلل لمعرّف ما في طابور خلال دورة واحدة، ثم يُرسل الجميع دفعة واحدة.

تطلق كتلة par التي تليها ثلاثة استدعاءات مجمّعة بالتوازي — gRPC batchGet إلى patient-service، وgRPC batchGetEncounters إلى clinical-service، واستدعاء REST إلى lab-service — وتُظهر patient-service الانضباط نفسه داخليًا: MGET على Redis أولًا، ثم استعلام PostgreSQL فقط لما لم يكن في الذاكرة المؤقتة. والملاحظة في الأسفل تلخص المكسب بوضوح: استدعاء gRPC واحد لكل خدمة، لا لكل صف مريض، مهما كان عدد المرضى على الشاشة.

08

تسلسل — حجز موعد ← تذكير فوري

حجز مزدوج لا يمكن أن يقع، وتذكير لا يمكن أن يضيع.

هنا يُلغى نمطان من الأعطال بالتصميم، لا يُكتشفان بعد وقوعهما. يمنع SET slot-hold NX EX 120 في Redis شخصين من التسابق على الفترة نفسها خلال الثواني التي يستغرقها ملء الاستمارة، وINSERT في Postgres أسفله محمي بقيد استبعاد على (practitioner_id, time_range) — فحتى لو اجتاز طلبان فحص Redis بطريقة ما، ترفض قاعدة البيانات نفسها الكتابة الثانية. صحة التزامن موجودة في المكان الوحيد القادر فعلًا على ضمانها: قاعدة البيانات، لا شيفرة تطبيق تأمل أنها تحققت في الوقت المناسب.

النصف الثاني هو نمط outbox قيد العمل: إدراج الموعد وإدراج outbox يتمان في معاملة قاعدة البيانات نفسها، لذا فإن حالة «نجح الحجز لكن لم يعلم به أحد» ليست حالة يمكن أن يقع فيها النظام — إذ يلتقط مستطلِع يعمل مستقلًا عن الطلب سطر outbox وينشر appointment.booked متى كان جاهزًا. ومن هناك يسلك التذكير المسار البطيء عن قصد: تنفذ آلية TTL مع dead-letter exchange في RabbitMQ مهلة الأربع والعشرين ساعة، وإذا فشل مزود الإشعارات، تُرفض الرسالة مع إعادتها إلى الطابور وتتراجع بشكل أُسّي قبل أن تستقر في dead-letter queue بدل أن تختفي.

09

تسلسل — قيمة مخبرية حرجة ← تنبيه فوري

من رسالة HL7 على الشبكة إلى هاتف يهتز، في قفزة Kafka Streams واحدة.

هذا هو التدفق الذي يدور حوله المشروع كله في الحقيقة — وهو الموصوف في الجملة الافتتاحية للمشروع. يتحدث جهاز المختبر بلغة HL7 v2، وهي صيغة من عام 1989 تحللها lab-service بواسطة HAPI HL7v2 وتتحقق منها وفق LOINC قبل حفظ أي شيء. ويأتي القرار المثير مباشرة بعد ذلك: بدل أن تقرر lab-service بنفسها ما هو حرج، فإنها تنشر الواقعة — lab.result.finalized — وتتولى طوبولوجيا Kafka Streams منفصلة، مقسمة حسب معرّف المريض مع إزالة للتكرار في نافذة منزلقة مدتها 15 دقيقة، مهمة الحكم. فصل الكشف عن التسجيل مقصود، حتى يمكن تغيير منطق التنبيه دون المساس بالخدمة التي تملك البيانات.

حين تتجاوز قيمة ما العتبة الحرجة، يتوزع نشر واحد على lab.critical-value على ثلاثة مستهلكين بالتوازي: توجهه notification-service إلى طابور ذي أولوية يتجاوز ساعات الهدوء كليًا، وتدفع chat-service إطار STOMP مباشرة إلى أي جلسة مفتوحة لدى الطبيب، ويحدّث اشتراك GraphQL لوحة تحكمه مباشرة. ثلاثة مسارات تسليم مختلفة، وحدث واحد، حتى يراه الطبيب أيًّا كانت الطريقة التي ينظر بها إلى النظام في تلك اللحظة.

10

تسلسل — إنشاء PDF ← إدخاله في RAG

ملف PDF موقّع لوصفة طبية يصبح شيئًا يمكن للمساعد الذكي الاستشهاد به.

التحقق من عدم التكرار في الأعلى هو أول ما يحدث، قبل أي عمل فعلي: تبحث document-service عن idempotencyKey الوارد في Postgres، فإن كان قد أُنشئ من قبل، تؤكد استلام الرسالة ولا تفعل شيئًا آخر. هذا الاستعلام الوحيد هو ما يجعل إعادة RabbitMQ تسليم أمر الإنشاء نفسه آمنة، دون أن ينتج أبدًا ملفي PDF موقّعين للوصفة نفسها.

بعد هذا الحاجز، يتكون المسار في الحقيقة من مسارين متتاليين. الأول ينشئ الملف، ويطبق عليه ملف أرشفة PDF/A، ويوقعه بتطبيق PAdES من BouncyCastle، ويخزن الكائن في MinIO حسب قيمة sha256 الخاصة به. ولا يبدأ الثاني إلا بعد أن يكتمل الأول تمامًا: يستخرج PDFBox النص من ملف PDF الذي كُتب للتو، وينشر document.text-extracted، وهذا الحدث وحده هو ما يجعل الوثيقة قابلة للاسترجاع من طرف مساعد المحادثة أصلًا — مقسمة إلى أجزاء، ومحوّلة إلى متجهات عبر Voyage AI، ومُدرجة في pgvector مع معرّف المريض كبيانات وصفية، وهو ما يسمح لاحقًا لاستعلام RAG بأن يقتصر على وثائق مريض واحد دون سواه.

11

تسلسل — المحادثة مع المساعد الذكي

يُتحقق من الموافقة قبل استرجاع أي وثيقة، ثم تُبث الرموز فور وصولها.

ترتيب العمليات هنا هو حجة الخصوصية كلها مجسّدة: تستدعي ai-service الدالة hasConsent(patientId, AI_ASSIST) لدى identity-service قبل تحويل السؤال إلى متجه، وقبل الاستعلام في pgvector، وقبل قراءة كلمة واحدة من ملف المريض. والموافقة الملغاة تُرجع PERMISSION_DENIED عند تلك النقطة وتتوقف المحادثة هناك — فلا يصل المساعد أبدًا إلى مرحلة يكون لديه فيها ما يحتاج إلى إخفائه.

بعد اجتياز الموافقة، يقتصر الاسترجاع على ذلك المريض وحده (topK 8، مع التصفية حسب patientId)، ويُنقّى السياق المسترجع من البيانات الصحية الشخصية قبل بناء التعليمات — فلا يرى النموذج أبدًا من التفاصيل المعرِّفة أكثر مما تتطلبه الإجابة. وملاحظة cache_control في خطوة بناء التعليمات مهمة للتكلفة بقدر أهميتها للسرعة: فتعليمات النظام ومتن الإرشادات السريرية ثابتان في كل سؤال يطرحه الفريق المعالج لهذا المريض، لذا يحوّل التخزين المؤقت للتعليمات لدى Anthropic معظم ذلك السياق إلى قراءة من الذاكرة المؤقتة بدل احتسابه من جديد، ويُسجَّل cacheReadTokens تحديدًا للتحقق من أن ذلك يحدث فعلًا بدل افتراضه. ومن هناك تصبح الحلقة مجرد تمرير مباشر: يبث Claude أحداث content_block_delta، وتعيد ai-service توجيه كل منها عبر تدفق gRPC من الخادم، وتحوّله chat-service إلى إطار STOMP، ويعرضه المتصفح تدريجيًا، فتظهر الإجابة كما يكتب الإنسان بدل أن تصل كتلة واحدة بعد انتظار.

12

تسلسل — وصفة طبية مع فحص التداخلات الدوائية

ذاكرة مؤقتة من مستويين أمام الفحص الوحيد الذي لا يمكن تخطيه.

يوجد تتابع الذاكرة المؤقتة Caffeine ثم Redis لأن validateCode تُستدعى في كل كتابة سريرية تقريبًا عبر النظام كله، واستعلام مصطلحات يذهب إلى Postgres في كل مرة سيبطئ كل تلك الكتابات دون مبرر — فمجموعات الرموز بالكاد تتغير، لذا فإن ذاكرة مؤقتة محلية L1 داخل العملية تدعمها ذاكرة مشتركة L2 هي الحل البديهي، وكل إخفاق يملأ المستويين في طريق العودة حتى يستفيد الطلب التالي في أي مكان من العنقود.

أما فحص التداخل نفسه فهو ما يجعل لهذا المخطط مكانه: النتيجة المتعارضة لا تُفشل الطلب، بل تُرجع 422 مع التعارض المحدد، ويمكن للطبيب تجاوزها — لكن فقط بتقديم مبرر يُحفظ مع الوصفة مع علامة تجاوز صريحة. لا شيء يُمنع في صمت ولا شيء يُسمح به في صمت؛ كل وصفة متنازع عليها تترك أثرًا لمن تجاوز ماذا ولماذا. والنشران الأخيران في الأسفل — prescription.issued إلى Kafka، وأمر إنشاء إلى RabbitMQ — هما بالضبط حيث يبدأ المخطط 4d.

تعمّق تقني

برمجيات الرعاية الصحية مشكلة أنظمة موزعة صعبة حقًا ترتدي زيًّا مملًّا. فيها متطلبات اتساق صارمة (لا يمكنك حجز غرفة عمليات مرتين)، ومتطلبات زمن استجابة صارمة (قيمة بوتاسيوم حرجة لا قيمة لها بعد ساعة من التأخير)، ومتطلبات امتثال صارمة (كل قراءة لملف مريض قابلة للتدقيق بحكم القانون)، وواجهة تكامل متباينة إلى أقصى حد (HL7 v2 من عام 1989 بجوار FHIR REST)، وبيانات صحية شخصية تجعل لكل قرار يخص تدفق البيانات عواقبه.

اخترته عن قصد. كان عملي السابق — بوابة Drupal/Next.js في Atos، ومنصة توصيل بـ Spring Boot في OpenTecc — متقن البناء لكنه أحادي البنية. كانت «الخدمات المصغرة» و«Kafka» مفاهيم أفهمها، لا منجزات أستطيع الإشارة إليها. وُجد هذا المشروع ليجعل هذا الادعاء قابلًا للإثبات، ولبناء شيء يمكن أن يساعد مستشفى فعلًا على العمل بأمان أكبر، لا لمجرد تضخيم قائمة التقنيات.

لماذا لن ينجح النظام الأحادي

أربع خصائص في هذا المجال تفرض التقسيم:

1

حِمل غير متكافئ إلى حد كبير

تعمل استعلامات المصطلحات في كل كتابة سريرية تقريبًا، بينما تعمل الفوترة مرة واحدة لكل لقاء. في النظام الأحادي تتقاسمان مجموعة الخيوط والذاكرة نفسها؛ فتضعف حلقة مصطلحات مكثفة عملية إنشاء الفواتير دون سبب.

2

ميزانيات زمن استجابة غير متكافئة إلى حد كبير

لتنبيه القيمة الحرجة اتفاقية مستوى خدمة بالثواني. أما إنشاء ملف PDF فقد يستغرق ثلاثين ثانية دون أن يهتم أحد. ووضعهما في مسار الطلب نفسه يعني أن الميزانية الأشد صرامة تفرض نفسها في كل مكان.

3

مجالات أعطال مستقلة

يجب ألا يمنع توقف Stripe طبيبًا من تسجيل لقاء. في النظام الأحادي، يجعل نطاق المعاملات المشترك ومجموعات الاتصالات المشتركة تحقيق هذا العزل صعبًا جدًا في الواقع.

4

نطاق الأثر التنظيمي

الخدمات التي تلمس البيانات الصحية الشخصية تحتاج إلى التشفير وبوابات الموافقة والتدقيق. والخدمات التي لا تلمسها لا ينبغي أن تدفع هذه الضريبة. أما النظام الأحادي فيُخضع كل مكوّن لهذه القيود افتراضيًا.

استراتيجية البروتوكولات

ثلاث وسائل نقل، لكل منها مبرر في جملة واحدة.

REST — كل واجهة برمجية خارجية أو عابرة للحدود. قابلة للتخزين المؤقت، ويمكن تتبعها بـ curl، ومدعومة أصلًا في المتصفح، وهي الخيار الافتراضي الصحيح. و FHIR R4 نفسه مبني على شكل REST، فتأتي قابلية التشغيل البيني في المجال الصحي مجانًا.

GraphQL — خدمة واحدة بالضبط، وهي BFF. تحتاج واجهة patient-360 في الواجهة الأمامية بيانات من خمس خدمات بشكل يحدده العميل. وفعل ذلك عبر REST يعني إما خمس رحلات ذهاب وإياب أو نقطة تجميع مخصصة لكل شاشة — Spring for GraphQL، بنهج schema-first، مع تفويض على مستوى الحقول وحدود لعمق الاستعلامات وتعقيدها.

gRPC — بين الخدمات الداخلية فقط، ومتمركز في terminology-service. حمولات أصغر من كيلوبايت يشكل فيها تغليف JSON جزءًا كبيرًا من الرسالة، تُستدعى في كل كتابة سريرية تقريبًا، بمخطط لا يتغير شكله أبدًا في الواقع. والجمع بين DataLoader و gRPC في BFF هو التصميم الذي أشير إليه أولًا: تبدو المحللات وكأنها تجلب البيانات حقلًا بحقل، لكن DataLoader يجمع كل المعرّفات خلال دورة واحدة من حلقة الأحداث ويوزعها باستدعاء gRPC واحد لكل خدمة — انظر مخطط Patient 360 أعلاه لترى كيف يحدث ذلك بالضبط.