Tuesday, September 23, 2025

Improving Software by Removing Features: The Case Against "Not For Advertising"

Paul (Founder)
Development
Developer thoughtfully working on simplifying software architecture in cafe setting

Adding features feels productive. Each new option promises flexibility, handles edge cases, and demonstrates progress. But every feature adds complexity—more code to maintain, more decisions for users, more mental overhead understanding how pieces interact. Sometimes the best product improvement is removing features that seemed necessary but prove problematic in practice.

Week 39 removed the "Not For Advertising" marketing status from LetAdmin. This status had seemed logical during design: properties might exist in the system without being advertised on portals. But production usage revealed it created confusion, overlapped with existing statuses, and obscured the distinction between a property's actual state and our advertising decisions about it.

This article explores why we removed "Not For Advertising," how the refactoring improved workflows, and what the experience teaches about building simpler, clearer software through thoughtful subtraction.

What Your Team Will Notice

The status dropdown in property management now shows five clear options instead of six: Available to Let, Under Negotiation, Let STC (Subject to Contract), Let, and Withdrawn. Each status describes the property's actual state in the letting process—where it sits in the journey from listing to tenancy.

When attempting to change a property's status to "Available to Let" or "Under Negotiation" (statuses that typically advertise), the system checks whether the property is currently advertising. If not, instead of allowing the status change, it displays guidance: "This property isn't currently advertising. To change status to an advertising status, use 'Start Advertising' or 'Edit Advert' first."

This separation clarifies the workflow: status reflects reality (is this property available, negotiating, or let?), whilst advertising reflects decisions (are we promoting this property on portals?). These are related but distinct concepts that the old "Not For Advertising" status conflated.

For properties that need to exist in the system without advertising—perhaps landlords haven't approved advertising yet, or agencies are preparing listings for future launch—staff simply don't click "Start Advertising." The property exists with appropriate status, and advertising remains off until deliberately enabled.

The Problem: Ambiguous Status Semantics

Property marketing statuses should describe objective reality. "Available to Let" means tenants can enquire and arrange viewings. "Let" means a tenancy is active. These statuses have clear business meanings that staff, landlords, and systems can understand uniformly.

"Not For Advertising" broke this pattern. It didn't describe the property's state in the letting process—it described our internal decision about whether to advertise it. This created several problems:

Semantic Overlap with "Withdrawn"

"Withdrawn" means the property is no longer available to let—perhaps the landlord decided not to let it, or market conditions changed. Practically, withdrawn properties don't advertise. So when would you use "Not For Advertising" versus "Withdrawn"?

Staff struggled with this distinction. If a landlord asked to stop advertising temporarily, should the property become "Not For Advertising" or "Withdrawn"? If you mark it "Withdrawn," would that confuse reporting about why properties left the market? If you mark it "Not For Advertising," does that accurately represent the landlord's intent?

Confusion About Default State

Should new properties default to "Not For Advertising" until staff explicitly starts advertising? Or should they default to "Available to Let" even before advertising begins? The former meant properties started in a status not reflecting their actual availability. The latter meant "Available to Let" didn't guarantee advertising.

Either choice created confusion. Default to "Not For Advertising," and staff changed status to "Available to Let" before having advertising-ready information, triggering premature portal sync attempts. Default to "Available to Let," and the status misled staff about whether properties were actually promoted.

Mixed Concerns in Status Logic

Code checking marketing status needed to understand two distinct concepts: the property's position in the letting lifecycle, and whether advertising was active. This mixed concerns that should remain separate:

# Before: Status conflates state and advertising decision
if property.marketing_status.in?(['available_to_let', 'under_negotiation'])
  # Should advertise... but what about 'not_advertised'?
  advertise_property(property)
elsif property.marketing_status == 'not_advertised'
  # Don't advertise... but is the property actually available?
  # Should portal sync remove it or preserve it?
end

Business logic became tangled with advertising logic, making code harder to reason about and maintain.

The Solution: Explicit Separation of Concerns

Removing "Not For Advertising" forced cleaner separation: marketing status describes reality, advertising state describes actions. Properties have five statuses reflecting actual letting state, and a separate boolean tracks whether advertising is active.

Updated Status Model

The simplified status enum clearly maps to business reality:

class Property
  MARKETING_STATUSES = {
    'available_to_let' => 'Available to Let',      # Ready for tenants
    'under_negotiation' => 'Under Negotiation',    # In active discussions
    'let_stc' => 'Let STC',                        # Agreed but not signed
    'let' => 'Let',                                # Tenancy active
    'withdrawn' => 'Withdrawn'                      # No longer available
  }.freeze

  validates :marketing_status,
            inclusion: { in: MARKETING_STATUSES.keys }

  # Separate boolean for advertising state
  def advertising?
    advertising == true
  end
end

Now code checking whether to advertise examines two independent concerns:

def should_advertise?
  # Status suggests advertising is appropriate
  status_allows_advertising? &&
  # Staff explicitly enabled advertising
  advertising? &&
  # All requirements met
  advertising_requirements_met?
end

def status_allows_advertising?
  marketing_status.in?(['available_to_let', 'under_negotiation'])
end

This separation makes logic clearer and makes each piece independently testable.

UI Guidance for Advertising Workflow

With "Not For Advertising" gone, the UI needed to guide staff through correct workflows. The status change modal now detects context and provides appropriate guidance:

<% if !@property.advertising? && target_status_requires_advertising?(new_status) %>
  <div class="alert alert-info">
    <h4>This property isn't currently advertising</h4>
    <p>
      To change status to <%= target_status_label(new_status) %>,
      the property should be advertising. Use
      <a href="<%= edit_advert_path(@property) %>">Edit Advert</a>
      or
      <a href="<%= start_advertising_path(@property) %>">Start Advertising</a>
      to set up advertising first.
    </p>
    <p>
      Alternatively, properties can have status <%= target_status_label(new_status) %>
      without advertising if that matches your workflow.
    </p>
  </div>
<% end %>

This contextual guidance prevents confusion whilst allowing flexibility—staff can have properties marked "Available to Let" without advertising if business needs require it, but they receive clear information about typical workflows.

Simplified Rightmove Integration

The old "Not For Advertising" status complicated portal sync. Should changing to "Not For Advertising" remove the property from Rightmove, or just mark it unavailable? Removal seemed logical but lost historical records. Marking unavailable seemed sensible but meant "Not For Advertising" properties still appeared in Rightmove's backend systems.

With "Not For Advertising" removed, Rightmove sync simplified:

class RightmoveSyncService
  def sync_property(property)
    # Always send property data to Rightmove
    # Never remove properties - preserve agency records
    serializer = RightmovePropertySerializer.new(property)
    api_service.send_property(serializer.payload)
  end

  # Status changes update existing Rightmove records
  # No special handling needed for advertising vs. non-advertising
end

Now status changes update Rightmove property records without complex "should we remove or update?" logic. Rightmove maintains complete property history, and advertising state changes flow through the normal webhook system.

Cognitive Load and Decision Fatigue

Every choice users make costs cognitive resources. Six status options require more mental overhead than five—not just evaluating one additional option, but considering how it relates to all others and when each is appropriate.

"Not For Advertising" created particular cognitive burden because its appropriateness depended on context staff couldn't easily evaluate. Should this property be "Available to Let" or "Not For Advertising"? That depends on whether you're planning to advertise soon, whether the landlord approved advertising, whether listing details are complete—factors requiring memory and judgement.

Removing the ambiguous option reduces decision points. Status reflects objective property state. Advertising is separate. Each decision is clearer with less context required.

Testing Simplified Workflows

Tests verify the streamlined workflow behaves correctly:

RSpec.describe Property do
  describe "status validation" do
    it "accepts five valid statuses" do
      valid_statuses = %w[available_to_let under_negotiation let_stc let withdrawn]

      valid_statuses.each do |status|
        property = build(:property, marketing_status: status)
        expect(property).to be_valid
      end
    end

    it "rejects 'not_advertised' status" do
      property = build(:property, marketing_status: 'not_advertised')
      expect(property).not_to be_valid
      expect(property.errors[:marketing_status]).to include('is not included in the list')
    end
  end

  describe "advertising eligibility" do
    it "allows advertising for available properties" do
      property = create(:property, marketing_status: 'available_to_let')
      expect(property.status_allows_advertising?).to be true
    end

    it "prevents advertising for let properties" do
      property = create(:property, marketing_status: 'let')
      expect(property.status_allows_advertising?).to be false
    end

    it "checks advertising state independently from status" do
      property = create(:property,
                       marketing_status: 'available_to_let',
                       advertising: false)

      expect(property.status_allows_advertising?).to be true
      expect(property.advertising?).to be false
    end
  end
end

These tests confirm status and advertising state remain independent concerns with clear validation rules.

Migration Path

Removing "Not For Advertising" required migrating existing properties using that status. We chose "Withdrawn" as the most appropriate mapping:

class RemoveNotAdvertisedStatus < ActiveRecord::Migration[7.0]
  def up
    # Update any properties with 'not_advertised' status
    Property.where(marketing_status: 'not_advertised')
            .update_all(marketing_status: 'withdrawn')
  end

  def down
    # Rollback would restore 'not_advertised' if needed
    # (In practice, unlikely to rollback after removing from codebase)
  end
end

This migration ensured no properties remained in an invalid state after the code change deployed.

Lessons About Feature Removal

This refactoring demonstrates several principles about simplifying software:

Question Features That Mix Concerns: "Not For Advertising" conflated property state with advertising decisions. When features handle multiple independent concepts, they're candidates for removal and refactoring into clearer separate mechanisms.

Watch for Semantic Overlap: Multiple features serving similar purposes suggest opportunities for consolidation. "Not For Advertising" and "Withdrawn" overlapped significantly—removing one clarified both concepts.

Measure Cognitive Load: Every option costs user attention. Features adding complexity without proportional value are better removed than maintained.

Prefer Explicit Over Implicit: Separate boolean flags for advertising state are more explicit than status values with implied advertising behaviour. Explicit mechanisms are clearer to understand and modify.

Trust Simpler Workflows: Concern that removing options would limit flexibility proved unfounded. Staff adapted quickly to clearer workflows with fewer but better-defined choices.

What's Next

The simplified status model enables clearer reporting and analytics. With five unambiguous statuses, we can accurately track how long properties spend in each stage, identify bottlenecks in letting workflows, and provide landlords clear visibility into property progress.

Future enhancements might include automated status transitions based on events (tenancy signed → automatically change to "Let"), status change approval workflows for quality control, and richer status history showing property journey through letting lifecycle.

By removing "Not For Advertising," we proved that thoughtful subtraction often improves software more than continued addition. Every feature removed simplifies mental models, reduces maintenance burden, and focuses attention on features that truly matter. Sometimes the best code is code you delete.