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
- Single Responsibility: Each part handles one business domain
- Encapsulation: Parts hide internal implementation
- Loose Coupling: Minimal dependencies between parts
- High Cohesion: Related functionality stays together
- 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
endInteractors (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
endModels
# 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
endServices
# 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
endPolicies (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
endInter-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
end2. 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
end3. 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])
end4. 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
endCreating 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.rb2. 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
end3. 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
end4. 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
end5. 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
end6. 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
endBest 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
end2. 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
end3. 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
endMigration 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
end3. 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
endFactory 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
endQuery 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
endDebugging 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.errorsNext Steps
- Review Design Patterns for detailed pattern usage
- Understand Database Schema for data organization
- Learn about Multi-Profile System for user management