Landlords come in various forms: individuals renting single properties, married couples managing joint portfolios, family trusts holding inheritance properties, and property management companies owning dozens of units. A robust landlord data model must accommodate this diversity whilst maintaining simplicity for the common case (individual owners) and avoiding over-engineering for edge cases.
Week 40 built LetAdmin's landlord management system from the ground up: defining the data model, implementing CRUD operations, designing management interfaces, and integrating landlords throughout property workflows. This article explores the design decisions that balance flexibility with maintainability.
What Your Team Will Notice
The landlords section appears in the main navigation, providing dedicated space for landlord management separate from property operations. Click through to see all landlords organized by type: individuals, joint individuals (co-owners treated as a unit), and organizations.
Creating a new landlord presents form fields adapting to landlord type. Individual landlords capture title, first name, last name, email, phone, and address. Joint individuals add a second set of name fields for the co-owner. Organizations replace personal titles and names with company name and registration number fields. The form shows only relevant fields for the selected type, avoiding cluttered interfaces with inapplicable options.
Each landlord's detail page displays their complete profile alongside properties they own (with ownership percentages for jointly-owned properties). Contact information, addresses, and notes provide comprehensive context for agency staff managing landlord relationships. Edit buttons allow quick updates without navigating away from the detail view.
From property pages, adding or changing landlords works through intuitive dropdowns populated with agency landlords. The ownership management modal (covered in the previous article) handles complex ownership scenarios, whilst simple sole-ownership cases remain straightforward single selections.
Under the Bonnet: Landlord Entity Design
The landlord model supports three primary types with type-specific attributes:
class Landlord < ApplicationRecord
belongs_to :agency
acts_as_tenant :agency
has_many :property_ownerships, dependent: :destroy
has_many :properties, through: :property_ownerships
# Landlord types
enum landlord_type: {
individual: 'individual',
joint: 'joint',
organisation: 'organisation'
}
# Individual/joint landlord attributes
validates :title, presence: true, if: :individual_or_joint?
validates :first_name, presence: true, if: :individual_or_joint?
validates :last_name, presence: true, if: :individual_or_joint?
# Joint landlord second person
validates :title_2, presence: true, if: :joint?
validates :first_name_2, presence: true, if: :joint?
validates :last_name_2, presence: true, if: :joint?
# Organization attributes
validates :company_name, presence: true, if: :organisation?
# Contact information (all types)
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP },
allow_blank: true
def full_name
case landlord_type
when 'individual'
"#{first_name} #{last_name}"
when 'joint'
"#{first_name} #{last_name} & #{first_name_2} #{last_name_2}"
when 'organisation'
company_name
end
end
def display_name
# Excludes titles for cleaner UI presentation
full_name
end
def salutation
case landlord_type
when 'individual'
"#{title} #{last_name}"
when 'joint'
# "Mr & Mrs Smith" or "Mr Smith & Ms Jones" depending on shared surname
if last_name == last_name_2
"#{title} & #{title_2} #{last_name}"
else
"#{title} #{last_name} & #{title_2} #{last_name_2}"
end
when 'organisation'
company_name
end
end
private
def individual_or_joint?
individual? || joint?
end
end
This design uses optional fields rather than separate tables for different landlord types. While separate tables (Single Table Inheritance or polymorphic associations) might seem cleaner architecturally, they complicate queries and associations. A single table with type-specific validation strikes better balance between flexibility and simplicity.
Smart Name Handling
The salutation method demonstrates thoughtful detail handling for joint landlords:
- Shared surname: "Mr & Mrs Smith" (common case)
- Different surnames: "Mr Smith & Ms Jones" (modern families, business partners)
- Title exclusion in
display_name: Removes titles for cleaner UI presentation throughout the application
These small touches improve user experience significantly compared to rigid formatting that doesn't accommodate real-world variations.
CRUD Operations and Controllers
Standard resourceful routes provide complete landlord management:
# config/routes.rb
resources :landlords do
member do
get :properties # Landlord's property portfolio
end
end
The controller implements typical CRUD with multi-tenant scoping:
class LandlordsController < ApplicationController
before_action :set_landlord, only: [:show, :edit, :update, :destroy]
def index
@landlords = Landlord.includes(:properties)
.order(created_at: :desc)
.page(params[:page])
.per(50)
end
def show
@properties = @landlord.properties
.includes(:property_ownerships)
.order(reference: :asc)
end
def new
@landlord = Landlord.new(landlord_type: 'individual')
end
def create
@landlord = Landlord.new(landlord_params)
@landlord.agency = current_tenant
if @landlord.save
redirect_to @landlord, notice: 'Landlord created successfully'
else
render :new, status: :unprocessable_entity
end
end
def update
if @landlord.update(landlord_params)
redirect_to @landlord, notice: 'Landlord updated successfully'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@landlord.destroy
redirect_to landlords_url, notice: 'Landlord removed successfully'
end
private
def set_landlord
@landlord = Landlord.find(params[:id])
end
def landlord_params
params.require(:landlord).permit(
:landlord_type, :title, :first_name, :last_name,
:title_2, :first_name_2, :last_name_2,
:company_name, :company_registration_number,
:email, :phone, :mobile,
:address_line_1, :address_line_2, :town, :county, :postcode,
:notes
)
end
end
The acts_as_tenant scoping automatically filters queries by current agency, preventing cross-tenant data access without explicit filtering in every query.
N+1 Query Prevention
The index action includes property associations to avoid N+1 queries when displaying property counts:
@landlords = Landlord.includes(:properties)
Without this eager loading, displaying "Landlord X owns 5 properties" for each landlord would trigger separate queries per landlord. The includes loads all associated properties in two queries total regardless of landlord count.
Dynamic Form Rendering
The landlord form adapts based on selected type:
<%= form_with(model: @landlord, local: true) do |form| %>
<!-- Landlord Type Selection -->
<div class="form-group">
<%= form.label :landlord_type, "Landlord Type" %>
<%= form.select :landlord_type,
options_for_select([
['Individual', 'individual'],
['Joint Individuals', 'joint'],
['Organisation', 'organisation']
], @landlord.landlord_type),
{},
class: 'form-select',
data: { action: 'change->landlord-form#updateFields' } %>
</div>
<!-- Individual/Joint Fields -->
<div data-landlord-form-target="individualFields"
style="display: <%= @landlord.individual? || @landlord.joint? ? 'block' : 'none' %>">
<div class="form-row">
<%= form.label :title %>
<%= form.select :title,
options_for_select(['Mr', 'Mrs', 'Miss', 'Ms', 'Dr', 'Prof'],
@landlord.title),
{ include_blank: 'Select...' },
class: 'form-select' %>
</div>
<div class="form-row">
<%= form.label :first_name %>
<%= form.text_field :first_name, class: 'form-control' %>
</div>
<div class="form-row">
<%= form.label :last_name %>
<%= form.text_field :last_name, class: 'form-control' %>
</div>
</div>
<!-- Joint Second Person Fields -->
<div data-landlord-form-target="jointFields"
style="display: <%= @landlord.joint? ? 'block' : 'none' %>">
<h4>Second Individual</h4>
<div class="form-row">
<%= form.label :title_2, "Title" %>
<%= form.select :title_2,
options_for_select(['Mr', 'Mrs', 'Miss', 'Ms', 'Dr', 'Prof'],
@landlord.title_2),
{ include_blank: 'Select...' },
class: 'form-select' %>
</div>
<div class="form-row">
<%= form.label :first_name_2, "First Name" %>
<%= form.text_field :first_name_2, class: 'form-control' %>
</div>
<div class="form-row">
<%= form.label :last_name_2, "Last Name" %>
<%= form.text_field :last_name_2, class: 'form-control' %>
</div>
</div>
<!-- Organisation Fields -->
<div data-landlord-form-target="organisationFields"
style="display: <%= @landlord.organisation? ? 'block' : 'none' %>">
<div class="form-row">
<%= form.label :company_name %>
<%= form.text_field :company_name, class: 'form-control' %>
</div>
<div class="form-row">
<%= form.label :company_registration_number %>
<%= form.text_field :company_registration_number,
class: 'form-control',
placeholder: 'e.g., 12345678' %>
</div>
</div>
<!-- Contact Information (all types) -->
<div class="form-section">
<h4>Contact Information</h4>
<div class="form-row">
<%= form.label :email %>
<%= form.email_field :email, class: 'form-control' %>
</div>
<div class="form-row">
<%= form.label :phone %>
<%= form.telephone_field :phone, class: 'form-control' %>
</div>
<div class="form-row">
<%= form.label :mobile %>
<%= form.telephone_field :mobile, class: 'form-control' %>
</div>
</div>
<!-- Address (all types) -->
<div class="form-section">
<h4>Address</h4>
<!-- Standard UK address fields -->
</div>
<%= form.submit class: 'btn btn-primary' %>
<% end %>
<script>
// Stimulus controller for dynamic field visibility
class LandlordFormController extends Controller {
static targets = ['individualFields', 'jointFields', 'organisationFields']
updateFields(event) {
const type = event.target.value
this.individualFieldsTarget.style.display =
(type === 'individual' || type === 'joint') ? 'block' : 'none'
this.jointFieldsTarget.style.display =
(type === 'joint') ? 'block' : 'none'
this.organisationFieldsTarget.style.display =
(type === 'organisation') ? 'block' : 'none'
}
}
</script>
This approach keeps all field definitions in one form whilst showing only relevant fields based on landlord type, balancing code maintainability with UX clarity.
Integration with Property Management
Properties integrate landlord associations throughout their interfaces:
<!-- Property show page landlord section -->
<div class="property-card">
<div class="card-header">
<h3><%= @property.jointly_owned? ? 'Landlords' : 'Landlord' %></h3>
<%= link_to 'Manage Ownership', '#',
data: { action: 'click->modal#open' },
class: 'btn btn-sm' %>
</div>
<div class="card-body">
<% if @property.property_ownerships.any? %>
<ul class="landlord-list">
<% @property.property_ownerships.includes(:landlord).each do |ownership| %>
<li>
<%= link_to ownership.landlord.display_name,
landlord_path(ownership.landlord) %>
<% if @property.jointly_owned? %>
<span class="ownership-badge">
<%= ownership.ownership_percentage %>%
</span>
<% end %>
</li>
<% end %>
</ul>
<% else %>
<p class="text-muted">No landlord assigned</p>
<%= link_to 'Assign Landlord', '#',
data: { action: 'click->modal#open' },
class: 'btn btn-outline' %>
<% end %>
</div>
</div>
This integration ensures landlord information remains accessible throughout property workflows without requiring staff to navigate away from property contexts.
Testing Landlord Types and Validation
Comprehensive tests verify type-specific validation and behavior:
RSpec.describe Landlord, type: :model do
describe "individual landlords" do
it "requires title, first name, and last name" do
landlord = build(:landlord, landlord_type: 'individual',
title: nil, first_name: nil, last_name: nil)
expect(landlord).not_to be_valid
expect(landlord.errors[:title]).to be_present
expect(landlord.errors[:first_name]).to be_present
expect(landlord.errors[:last_name]).to be_present
end
it "formats full_name correctly" do
landlord = build(:landlord, landlord_type: 'individual',
first_name: 'Jane', last_name: 'Smith')
expect(landlord.full_name).to eq('Jane Smith')
end
end
describe "joint landlords" do
it "requires both sets of names" do
landlord = build(:landlord, landlord_type: 'joint',
first_name: 'Jane', last_name: 'Smith',
first_name_2: nil, last_name_2: nil)
expect(landlord).not_to be_valid
expect(landlord.errors[:first_name_2]).to be_present
expect(landlord.errors[:last_name_2]).to be_present
end
it "formats joint names correctly" do
landlord = build(:landlord, landlord_type: 'joint',
first_name: 'Jane', last_name: 'Smith',
first_name_2: 'John', last_name_2: 'Smith')
expect(landlord.full_name).to eq('Jane Smith & John Smith')
end
it "handles different surnames appropriately" do
landlord = build(:landlord, landlord_type: 'joint',
title: 'Ms', first_name: 'Jane', last_name: 'Smith',
title_2: 'Mr', first_name_2: 'John', last_name_2: 'Jones')
expect(landlord.salutation).to eq('Ms Smith & Mr Jones')
end
end
describe "organisation landlords" do
it "requires company name" do
landlord = build(:landlord, landlord_type: 'organisation',
company_name: nil)
expect(landlord).not_to be_valid
expect(landlord.errors[:company_name]).to be_present
end
it "does not require individual names" do
landlord = build(:landlord, landlord_type: 'organisation',
company_name: 'Smith Properties Ltd',
first_name: nil, last_name: nil)
expect(landlord).to be_valid
end
it "uses company name as full_name" do
landlord = build(:landlord, landlord_type: 'organisation',
company_name: 'Smith Properties Ltd')
expect(landlord.full_name).to eq('Smith Properties Ltd')
end
end
end
These tests ensure validation logic remains correct as landlord functionality evolves.
What's Next
The landlord data model enables rich features: landlord statements showing rental income and expenses per owner, landlord portals allowing direct access to property information, landlord document management for contracts and compliance paperwork, and sophisticated reporting analyzing portfolio performance by landlord type and ownership structure.
Future enhancements might include landlord communication templates (email templates for common scenarios like rent reviews or maintenance notifications), landlord approval workflows (requiring landlord sign-off for major property decisions), and landlord relationship scoring (identifying engaged versus hands-off landlords for tailored service approaches).
By designing a flexible landlord model that accommodates common cases simply whilst supporting complex scenarios, LetAdmin provides the foundation for comprehensive landlord relationship management that scales from small agencies managing dozens of landlords to large operations serving hundreds.