Kulcare Docs
GeneralArchitecture

Parts System - Deep Dive

The Parts System is the core architectural pattern of the Kulcare API Backend, providing a modular, domain-driven approach to organizing code. This document provides a comprehensive understanding of how the parts system works and how to effectively use it.

Philosophy & Principles

Why Parts?

Traditional Rails applications often suffer from:

  • Monolithic models that become unwieldy
  • Controllers scattered across the application
  • Business logic mixed with presentation logic
  • Difficult team collaboration on large codebases
  • Hard to extract microservices later

The Parts System solves these by:

  • Domain isolation: Each part owns its domain completely
  • Clear boundaries: Explicit interfaces between parts
  • Team scalability: Teams can own specific parts
  • Progressive decoupling: Easy path to microservices
  • Maintainability: Smaller, focused codebases

Core Principles

  1. Single Responsibility: Each part handles one business domain
  2. Encapsulation: Parts hide internal implementation
  3. Loose Coupling: Minimal dependencies between parts
  4. High Cohesion: Related functionality stays together
  5. Domain Ownership: Parts own their data and logic

Anatomy of a Part

Standard Structure

parts/clinic/                       # Domain root
├── controllers/                    # HTTP endpoints
│   ├── appointments_controller.rb
│   ├── clinic_doctors_controller.rb
│   └── clinics_controller.rb
├── interactors/                    # Business logic
│   ├── create_appointment.rb
│   ├── schedule_doctor.rb
│   └── update_clinic_settings.rb
├── models/                         # Domain models
│   ├── clinic.rb
│   ├── clinic_doctor.rb
│   └── appointment.rb
├── services/                       # Support services
│   ├── appointment_scheduler.rb
│   └── notification_sender.rb
├── policies/                       # Authorization
│   ├── clinic_policy.rb
│   └── appointment_policy.rb
├── serializers/                    # API responses
│   ├── clinic_serializer.rb
│   └── appointment_serializer.rb
├── migrate/                        # Migrations
│   └── 20240113_create_clinics.rb
├── routes.rb                       # Route definitions
├── seeds.rb                        # Seed data
└── spec/                          # Tests
    ├── controllers/
    ├── interactors/
    └── models/

Component Responsibilities

Controllers

# parts/clinic/controllers/clinics_controller.rb
class ClinicsController < ApplicationController
  def create
    # Thin controller - delegates to interactor
    result = CreateClinic.call(clinic_params)
    
    if result.success?
      render json: result.clinic, serializer: ClinicSerializer
    else
      render_error(result.error)
    end
  end
end

Interactors (Business Logic)

# parts/clinic/interactors/create_clinic.rb
class CreateClinic < BaseInteractor
  delegate :params, :current_user, to: :context
  
  def call
    validate_params
    create_clinic
    setup_defaults
    send_welcome_email
  rescue StandardError => e
    context.fail!(error: e.message)
  end
  
  private
  
  def validate_params
    unless params[:name].present?
      context.fail!(error: "Name is required")
    end
  end
  
  def create_clinic
    context.clinic = Clinic.create!(
      name: params[:name],
      owner: current_user
    )
  end
  
  def setup_defaults
    CreateDefaultSettings.call(clinic: context.clinic)
    CreateDefaultSchedule.call(clinic: context.clinic)
  end
  
  def send_welcome_email
    ClinicMailer.welcome(context.clinic).deliver_later
  end
end

Models

# parts/clinic/models/clinic.rb
class Clinic < ApplicationRecord
  # Associations
  has_many :clinic_doctors
  has_many :appointments
  has_many :clinic_patients
  has_one :clinic_setting
  
  # Validations
  validates :name, presence: true
  validates :phone_number, format: { with: /\A\d{10}\z/ }
  
  # Scopes
  scope :active, -> { where(deleted: false) }
  scope :verified, -> { where(verified: true) }
  
  # Domain methods
  def total_patients
    clinic_patients.active.count
  end
  
  def available_slots(date)
    AvailabilityCalculator.new(self, date).slots
  end
end

Services

# parts/clinic/services/appointment_scheduler.rb
class AppointmentScheduler
  def initialize(clinic, doctor)
    @clinic = clinic
    @doctor = doctor
  end
  
  def next_available_slot
    # Complex scheduling logic
  end
  
  def book_slot(patient, datetime)
    # Booking logic
  end
end

Policies (Authorization)

# parts/clinic/policies/clinic_policy.rb
class ClinicPolicy < ApplicationPolicy
  def create?
    user.can_create_clinic?
  end
  
  def update?
    user.owns_clinic?(record) || user.admin?
  end
  
  def destroy?
    user.owns_clinic?(record) && record.can_be_deleted?
  end
end

Inter-Part Communication

Communication Patterns

1. Direct Model Access (Acceptable for Core Models)

# In parts/clinic/interactors/create_appointment.rb
class CreateAppointment < BaseInteractor
  def call
    # Accessing User from core
    user = User.find(context.user_id)
    
    # Accessing Patient from patient part
    patient = Patient.find(context.patient_id)
    
    # Create appointment in clinic part
    Appointment.create!(
      clinic: context.clinic,
      patient: patient,
      doctor: context.doctor
    )
  end
end

2. Service Objects for Complex Operations

# In parts/billing/services/invoice_generator.rb
class InvoiceGenerator
  def generate_for_appointment(appointment)
    # Billing part handles invoice generation
    # for appointment from clinic part
  end
end

# Used in parts/clinic/interactors/complete_appointment.rb
class CompleteAppointment < BaseInteractor
  def call
    # Complete appointment logic
    appointment.update!(status: 'completed')
    
    # Delegate billing to billing part
    Billing::InvoiceGenerator.new.generate_for_appointment(appointment)
  end
end

3. Event-Driven Communication

# In parts/clinic/interactors/create_appointment.rb
class CreateAppointment < BaseInteractor
  def call
    appointment = Appointment.create!(appointment_params)
    
    # Publish event for other parts to consume
    EventBus.publish('appointment.created', {
      appointment_id: appointment.id,
      patient_id: appointment.patient_id,
      clinic_id: appointment.clinic_id
    })
  end
end

# In parts/notification/subscribers/appointment_subscriber.rb
EventBus.subscribe('appointment.created') do |event|
  SendAppointmentNotification.perform_later(event[:appointment_id])
end

4. Shared Interfaces

# In parts/core/interfaces/notifiable.rb
module Notifiable
  def send_sms(message)
    SmsService.new.send(phone_number, message)
  end
  
  def send_email(subject, body)
    EmailService.new.send(email, subject, body)
  end
end

# Used in multiple parts
class Patient < ApplicationRecord
  include Notifiable
end

class Doctor < ApplicationRecord
  include Notifiable
end

Creating a New Part

Step-by-Step Guide

1. Create Directory Structure

# Create the part
mkdir -p parts/pharmacy
cd parts/pharmacy

# Create subdirectories
mkdir controllers interactors models services policies serializers migrate spec

# Create routes file
touch routes.rb
touch seeds.rb

2. Define Routes

# parts/pharmacy/routes.rb
Rails.application.routes.draw do
  scope module: :pharmacy do
    resources :pharmacies do
      resources :medicines
      resources :orders
    end
  end
  
  namespace :pharmacy do
    resources :prescriptions, only: [:show, :update]
  end
end

3. Create Base Model

# parts/pharmacy/models/pharmacy.rb
class Pharmacy < ApplicationRecord
  belongs_to :owner, class_name: 'User'
  has_many :medicine_stocks
  has_many :orders
  
  validates :name, presence: true
  validates :license_number, presence: true, uniqueness: true
end

4. Create Controller

# parts/pharmacy/controllers/pharmacies_controller.rb
class PharmaciesController < ApplicationController
  before_action :authenticate_user!
  
  def index
    pharmacies = current_user.pharmacies
    render json: pharmacies, each_serializer: PharmacySerializer
  end
  
  def create
    result = CreatePharmacy.call(
      params: pharmacy_params,
      user: current_user
    )
    
    if result.success?
      render json: result.pharmacy, serializer: PharmacySerializer
    else
      render_error(result.error)
    end
  end
end

5. Create Interactor

# parts/pharmacy/interactors/create_pharmacy.rb
class CreatePharmacy < BaseInteractor
  def call
    validate_license
    create_pharmacy
    setup_inventory
    notify_admin
  end
  
  private
  
  def validate_license
    unless LicenseValidator.valid?(context.params[:license_number])
      context.fail!(error: "Invalid license number")
    end
  end
  
  def create_pharmacy
    context.pharmacy = Pharmacy.create!(
      context.params.merge(owner: context.user)
    )
  end
end

6. Create Migration

# parts/pharmacy/migrate/20240115_create_pharmacies.rb
class CreatePharmacies < ActiveRecord::Migration[7.1]
  def change
    create_table :pharmacies do |t|
      t.string :name, null: false
      t.string :license_number, null: false
      t.references :owner, foreign_key: { to_table: :users }
      t.jsonb :settings, default: {}
      t.boolean :deleted, default: false
      
      t.timestamps
    end
    
    add_index :pharmacies, :license_number, unique: true
    add_index :pharmacies, :deleted
  end
end

Best Practices

1. Part Independence

# ❌ Bad: Direct dependency
class Clinic < ApplicationRecord
  def calculate_bill
    # Clinic shouldn't know billing internals
    bill = Bill.new
    bill.add_consultation_fee(consultation_fee)
    bill.calculate_tax
    bill.total
  end
end

# ✅ Good: Use service from billing part
class Clinic < ApplicationRecord
  def calculate_bill
    Billing::Calculator.new(self).total
  end
end

2. Clear Interfaces

# ❌ Bad: Exposing internals
class AppointmentScheduler
  attr_accessor :slots, :buffer_time, :break_times
  
  def initialize
    @slots = []
    @buffer_time = 5
    @break_times = []
  end
end

# ✅ Good: Clean interface
class AppointmentScheduler
  def initialize(clinic, date)
    @clinic = clinic
    @date = date
  end
  
  def available_slots
    # Returns array of available time slots
  end
  
  def book_slot(time, patient)
    # Books the slot, returns appointment
  end
end

3. Consistent Naming

# Parts should follow naming conventions:
# - Parts: singular, lowercase (clinic, not clinics)
# - Controllers: plural (clinics_controller.rb)
# - Models: singular (clinic.rb)
# - Tables: plural (clinics)

4. Testing Within Parts

# spec/parts/clinic/interactors/create_appointment_spec.rb
RSpec.describe CreateAppointment do
  describe '#call' do
    context 'with valid params' do
      it 'creates appointment' do
        result = CreateAppointment.call(valid_params)
        expect(result.success?).to be true
        expect(result.appointment).to be_persisted
      end
    end
    
    context 'with conflicting time' do
      it 'fails with error' do
        create(:appointment, time: '10:00')
        result = CreateAppointment.call(time: '10:00')
        expect(result.failure?).to be true
        expect(result.error).to include('already booked')
      end
    end
  end
end

Migration Strategy to Microservices

The parts system is designed to enable gradual extraction to microservices:

1. Identify Extraction Candidate

# Parts with clear boundaries are good candidates:
# - billing (clear domain, external service integration)
# - notification (message queue based)
# - abdm (external API integration)

2. Create Service Interface

# Create adapter in main app
class BillingService
  def self.create_invoice(appointment)
    if ENV['BILLING_SERVICE_URL']
      # Call external service
      HTTParty.post("#{ENV['BILLING_SERVICE_URL']}/invoices", 
        body: appointment.to_json)
    else
      # Use local implementation
      Billing::InvoiceGenerator.new.generate(appointment)
    end
  end
end

3. Extract Gradually

  • Start with read operations
  • Move write operations
  • Migrate data
  • Remove local implementation

Common Patterns

Repository Pattern for Data Access

# parts/clinic/repositories/appointment_repository.rb
class AppointmentRepository
  def find_by_date(clinic, date)
    Appointment.where(clinic: clinic, date: date)
  end
  
  def available_slots(clinic, date)
    # Complex query logic
  end
end

Factory Pattern for Object Creation

# parts/clinic/factories/appointment_factory.rb
class AppointmentFactory
  def self.create_from_request(request_params)
    Appointment.new(
      patient: find_or_create_patient(request_params[:patient]),
      doctor: find_doctor(request_params[:doctor_id]),
      time: parse_time(request_params[:time])
    )
  end
end

Query Objects for Complex Queries

# parts/clinic/queries/available_doctors_query.rb
class AvailableDoctorsQuery
  def initialize(clinic, datetime)
    @clinic = clinic
    @datetime = datetime
  end
  
  def call
    @clinic.doctors
           .joins(:schedules)
           .where(schedules: { day: day_of_week })
           .where.not(id: booked_doctor_ids)
  end
end

Debugging Parts

Finding Code

# Find all controllers in clinic part
find parts/clinic -name "*_controller.rb"

# Find interactor usage
grep -r "CreateAppointment" parts/

# Find model references
grep -r "belongs_to :clinic" parts/

Rails Console

# Load specific part in console
require_relative 'parts/clinic/models/clinic'

# Test interactor
result = CreateAppointment.call(params)
puts result.success?
puts result.errors

Next Steps

On this page