} .article-content code { font-family: "SF Mono", "Monaco", "Cascadia Code", "Roboto Mono", monospace; font-size: 0.9em; } .video-placeholder { background: linear-gradient( 135deg, rgba(27, 150, 255, 0.1), rgba(255, 107, 53, 0.1) ); border: 2px dashed rgba(27, 150, 255, 0.3); border-radius: 8px; padding: 3rem; text-align: center; } .repo-link { background: linear-gradient( 135deg, rgba(27, 150, 255, 0.05), rgba(255, 107, 53, 0.05) ); border-left: 4px solid #1b96ff; padding: 1rem; border-radius: 0 6px 6px 0; }
Salesforce Development

🌦️ Building Enterprise Salesforce Apps with fflib Patterns

Deep-dive into building a Salesforce weather dashboard using enterprise patterns, domain logic, and modern development practices

Gábor Demeter
Gábor Demeter
Lead Software Engineer
January 2026
12 min read
Salesforce fflib Patterns LWC API Integration Enterprise Architecture

🎥 Video Walkthrough Coming Soon!

I'll be adding a detailed video walkthrough covering:

  • Complete architecture overview
  • Live coding demonstration
  • Testing strategy walkthrough
  • Design pattern explanations
  • API integration best practices
  • Production deployment demo

📂 Project Repository

Complete source code, documentation, and deployment scripts

The Challenge: Building Maintainable Salesforce Applications

When building enterprise Salesforce applications, proper architecture becomes crucial for maintainability, testability, and scalability. Too often, I see Salesforce projects that start simple but become unmaintainable as they grow. The fflib (FinancialForce Library) framework provides proven patterns that help organize your Apex code following Domain-Driven Design principles.

In this post, I'll walk through my Weather Dashboard application - a complete Salesforce implementation that demonstrates how to structure enterprise-grade applications using modern development practices.

Architecture Overview: The Four Layers

The fflib framework organizes code into four distinct layers, each with a specific responsibility:

🏛️ Domain Layer

Business logic and validation rules

🔍 Selector Layer

Encapsulated SOQL queries

⚙️ Service Layer

Complex business processes

🔄 Unit of Work

Transactional operations

Domain Layer: Business Logic Made Clear

Let's start with the Domain layer. Here's how I implemented the WeatherReports domain class:

/**
 * Domain class for Weather Report records following Enterprise Patterns
 */
public class WeatherReports extends fflib_SObjectDomain {
    public WeatherReports(List<Weather_Report__c> sObjectList) {
        super(sObjectList);
    }

    public class Constructor implements fflib_SObjectDomain.IConstructable {
        public fflib_SObjectDomain construct(List<SObject> sObjectList) {
            return new WeatherReports(sObjectList);
        }
    }

    /**
     * Domain logic for validating weather reports
     */
    public override void onBeforeInsert() {
        validateWeatherReports();
        setReportDateTime();
    }

    public override void onBeforeUpdate(Map<Id, SObject> existingRecords) {
        validateWeatherReports();
    }

    /**
     * Validate weather report data
     */
    private void validateWeatherReports() {
        for (Weather_Report__c report : getWeatherRecords()) {
            validateTemperature(report);
            validateHumidity(report);
            validateCity(report);
        }
    }

    /**
     * Set report date time for new records
     */
    private void setReportDateTime() {
        for (Weather_Report__c report : getWeatherRecords()) {
            if (report.Report_Date_Time__c == null) {
                report.Report_Date_Time__c = Datetime.now();
            }
        }
    }
}

✅ Key Benefits of This Approach:

  • Centralized Business Logic: All validation rules in one place
  • Trigger Handler Pattern: Clean trigger management
  • Reusable Validation: Logic can be called from anywhere
  • Easy Testing: Domain logic is isolated and testable

Selector Pattern: SOQL Done Right

The Selector pattern encapsulates all database queries and provides a consistent interface for data access. Here's my WeatherReportsSelector implementation:

/**
 * Selector class for Weather Reports following Enterprise Patterns
 */
public class WeatherReportsSelector extends fflib_SObjectSelector {
    
    public static WeatherReportsSelector newInstance() {
        return (WeatherReportsSelector) Application.Selector.newInstance(Weather_Report__c.SObjectType);
    }

    public Schema.SObjectType getSObjectType() {
        return Weather_Report__c.SObjectType;
    }

    public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField> {
            Weather_Report__c.Id,
            Weather_Report__c.Name,
            Weather_Report__c.City__c,
            Weather_Report__c.Temperature__c,
            Weather_Report__c.Weather_Description__c,
            Weather_Report__c.Humidity__c,
            Weather_Report__c.Pressure__c,
            Weather_Report__c.Report_Date_Time__c,
            Weather_Report__c.CreatedDate,
            Weather_Report__c.LastModifiedDate
        };
    }

    /**
     * Select records by city with limit
     */
    public List<Weather_Report__c> selectByCity(String city, Integer limitCount) {
        return (List<Weather_Report__c>) Database.query(
            newQueryFactory()
                .setCondition('City__c = :city')
                .setLimit(limitCount)
                .setOrdering('Report_Date_Time__c', fflib_QueryFactory.SortOrder.DESCENDING)
                .toSOQL()
        );
    }

    /**
     * Select recent reports across all cities
     */
    public List<Weather_Report__c> selectRecentReports(Integer limitCount) {
        return (List<Weather_Report__c>) Database.query(
            newQueryFactory()
                .setLimit(limitCount)
                .setOrdering('Report_Date_Time__c', fflib_QueryFactory.SortOrder.DESCENDING)
                .toSOQL()
        );
    }
}

🔍 Why This Pattern Matters:

  • Consistent Field Selection: Always query the same fields
  • Security Handled: FLS and CRUD checks are automatic
  • Query Optimization: Built-in query factory for complex SOQL
  • Mockable for Testing: Easy to create test data scenarios

Service Layer: External API Integration

The Service layer handles complex business processes. In our weather app, this includes calling the OpenWeatherMap API and managing the data flow:

/**
 * Weather Service Implementation with proper error handling
 */
public class WeatherServiceImpl implements IWeatherService {
    
    /**
     * Get current weather from OpenWeatherMap API
     */
    public WeatherData getCurrentWeather(String city) {
        try {
            Http http = new Http();
            HttpRequest request = new HttpRequest();
            
            String endpoint = 'callout:OpenWeatherMap_API' + 
                            '/weather?q=' + EncodingUtil.urlEncode(city, 'UTF-8') + 
                            '&appid={!$Credential.OWM_API_Key}&units=metric';
            
            request.setEndpoint(endpoint);
            request.setMethod('GET');
            request.setTimeout(10000); // 10 second timeout
            
            HttpResponse response = http.send(request);
            
            if (response.getStatusCode() == 200) {
                return parseWeatherResponse(response.getBody(), city);
            } else {
                throw new WeatherServiceException('API call failed: ' + response.getStatus());
            }
        } catch (Exception e) {
            System.debug('Error in getCurrentWeather: ' + e.getMessage());
            throw new WeatherServiceException('Failed to get weather data: ' + e.getMessage());
        }
    }

    /**
     * Save weather report using Unit of Work pattern
     */
    public Id saveWeatherReport(WeatherData weatherData) {
        try {
            // Create Unit of Work
            fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
            
            // Create Weather Report record
            Weather_Report__c report = new Weather_Report__c(
                City__c = weatherData.city,
                Temperature__c = weatherData.temperature,
                Weather_Description__c = weatherData.description,
                Humidity__c = weatherData.humidity,
                Pressure__c = weatherData.pressure,
                Report_Date_Time__c = weatherData.reportDateTime
            );
            
            // Register for insert
            uow.registerNew(report);
            
            // Commit changes
            uow.commitWork();
            
            return report.Id;
        } catch (Exception e) {
            System.debug('Error in saveWeatherReport: ' + e.getMessage());
            throw new WeatherServiceException('Failed to save weather report: ' + e.getMessage());
        }
    }
}

Lightning Web Component: Modern UI

The frontend uses Lightning Web Components with clean separation between UI logic and business logic:

// weatherDashboard.js - Clean, reactive component
import { LightningElement, track, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getCurrentWeatherAndSave from '@salesforce/apex/WeatherDashboardController.getCurrentWeatherAndSave';
import getRecentWeatherReports from '@salesforce/apex/WeatherDashboardController.getRecentWeatherReports';

export default class WeatherDashboard extends LightningElement {
    @track cityName = '';
    @track currentWeather = null;
    @track recentReports = [];
    @track isLoading = false;

    // Wired method for reactive data
    @wire(getRecentWeatherReports, { limitCount: 10 })
    wiredGetRecentReports(result) {
        this.wiredRecentReports = result;
        if (result.data) {
            this.recentReports = result.data;
        }
    }

    handleCityChange(event) {
        this.cityName = event.target.value;
    }

    async handleGetWeather() {
        if (!this.cityName.trim()) {
            this.showToast('Error', 'Please enter a city name', 'error');
            return;
        }

        this.isLoading = true;
        try {
            const result = await getCurrentWeatherAndSave({ city: this.cityName.trim() });
            
            if (result.success) {
                this.currentWeather = result.weatherData;
                this.showToast('Success', result.message, 'success');
                
                // Refresh recent reports
                return refreshApex(this.wiredRecentReports);
            } else {
                this.showToast('Error', result.message, 'error');
            }
        } catch (error) {
            this.showToast('Error', 'Unexpected error occurred', 'error');
        } finally {
            this.isLoading = false;
        }
    }
}

Testing Strategy: Comprehensive Coverage

One of the biggest advantages of the fflib architecture is how easy it makes testing. Each layer can be tested independently, and you can mock dependencies cleanly.

🧪 Test Coverage Breakdown:

  • Domain Tests: Validation logic (95% coverage)
  • Selector Tests: Query methods (90% coverage)
  • Service Tests: API integration (88% coverage)
  • Controller Tests: LWC backend (92% coverage)
  • Integration Tests: End-to-end flows (85% coverage)
  • Overall Coverage: 91% across all Apex classes

Key Architectural Decisions & Lessons Learned

✅ What Worked Well

  • Clear separation of concerns
  • Excellent testability
  • Easy to extend with new features
  • Named credentials for secure API calls
  • Proper error handling throughout
  • Reactive UI with @wire decorators

⚠️ Challenges & Solutions

  • API Rate Limits: Implemented caching strategy
  • Bulk Operations: Used Unit of Work for efficiency
  • Error Handling: Consistent exception patterns
  • Testing External APIs: Mock callouts properly
  • Complex SOQL: Query factory made it manageable

Next Steps & Future Enhancements

This weather dashboard demonstrates the foundation of enterprise Salesforce development. Here's what I'm planning next:

🚀 Planned Features

  • Weather forecasts and alerts
  • Geolocation-based weather
  • Historical weather analysis
  • Mobile app with offline support

🔧 Technical Improvements

  • Platform Events for real-time updates
  • Einstein Analytics dashboards
  • Advanced caching with Platform Cache
  • Continuous Integration with GitHub Actions

📂 Complete Source Code

Browse the complete implementation including tests, deployment scripts, and documentation.

View Repository