🎥 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