TrailForceTips

Apex Annotations in Salesforce: A Practical Guide

Apex Annotations in Salesforce let you attach metadata to classes, methods, and properties so the platform knows how to treat your code at runtime. Used well, they make components faster, triggers safer, and tests clearer. This guide explains the most common annotations, when to use them, and the pitfalls to avoid — with practical code you can paste into your org.

Understanding Apex Annotations in Salesforce is essential for every developer who wants to write efficient, reusable, and secure Apex code. These annotations define how your classes interact with Lightning components, Flows, and the Salesforce runtime itself.

Apex Annotations in Salesforce illustration
A visual overview of common Apex annotations and where they apply.

This article dives deep into Apex Annotations in Salesforce, explaining each annotation’s purpose, syntax, and practical use cases with examples. By mastering Apex Annotations in Salesforce, you’ll be able to write cleaner code that integrates seamlessly with the platform’s automation and UI layers.

1. @AuraEnabled

Expose Apex to Lightning. Methods or properties marked with @AuraEnabled can be invoked by Aura components or Lightning Web Components (LWC). With LWC, pair it with cacheable=true for read-only, cacheable methods.

Use Case: Fetching Account Records by Industry (LWC-friendly)

public with sharing class AccountService {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccountsByIndustry(String industry) {
        return [
            SELECT Id, Name, Industry
            FROM Account
            WHERE Industry = :industry
            ORDER BY Name
            LIMIT 200
        ];
    }
}

Aura example (controller bound): your original Aura snippet works. For LWC, you’d import getAccountsByIndustry from '@salesforce/apex/AccountService.getAccountsByIndustry' and wire it with a reactive parameter.

In this section, you’ll see how Apex Annotations in Salesforce work in real Lightning use cases — especially for exposing server-side logic to components using @AuraEnabled.

Gotchas: Methods with DML or callouts cannot be cacheable=true. Prefer wrappers/DTOs when returning complex shapes to components.

2. @Future

Run work asynchronously in a separate thread. Useful for callouts or heavy processing kicked off from triggers. Requires static methods and primitive parameters.

Use Case: Callout to an External System After Insert

public with sharing class IntegrationService {
    @Future(callout=true)
    public static void sendAccountData(Id accountId) {
        Account acc = [
            SELECT Id, Name, Industry
            FROM Account WHERE Id = :accountId
        ];

        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.externalsystem.com/account');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(new Map<String,Object>{
            'id' => acc.Id, 'name' => acc.Name, 'industry' => acc.Industry
        }));

        Http http = new Http();
        HttpResponse res = http.send(req);
        System.debug('Response: ' + res.getStatusCode() + ' / ' + res.getBody());
    }
}

trigger AccountAfterInsert on Account (after insert) {
    for (Account acc : Trigger.new) {
        IntegrationService.sendAccountData(acc.Id);
    }
}

Among the most powerful Apex Annotations in Salesforce is @Future, which enables asynchronous execution for long-running or external callout operations.

Consider: prefer Queueable for complex chains/retry logic. Use Platform Events or Change Data Capture if you integrate multiple systems.

3. @InvocableMethod (and @InvocableVariable)

Makes methods available to Flow/Process Builder. Great for declarative automation with strongly typed Apex behind the scenes. Pair with @InvocableVariable inside custom request/response classes to pass structured data.

Use Case: Sending a Greeting from Flow

public with sharing class GreetingService {

    public class Request {
        @InvocableVariable(required=true) public String name;
    }

    @InvocableMethod(label='Send Greeting' description='Sends a greeting message.')
    public static void sendGreeting(List<Request> inputs) {
        for (Request r : inputs) {
            System.debug('Hello, ' + r.name + '!');
        }
    }
}

Using Apex Annotations in Salesforce like @InvocableMethod gives admins and developers the flexibility to combine Apex with declarative automation tools such as Flow and Process Builder.

4. @IsTest, @TestSetup, @SeeAllData

Testing annotations define what’s test-only, how to seed data once per class, and (rarely) allow real org data. Prefer synthetic data over @SeeAllData=true.

Use Case: Trigger with Dependencies (Mockable)

public class TaskService {
    public virtual void createTask(Id accountId) {
        insert new Task(Subject = 'Follow-up', WhatId = accountId);
    }
}

public class AccountTriggerHandler {
    public static void handleAfterInsert(List<Account> newAccounts) {
        TaskService service = new TaskService();
        for (Account acc : newAccounts) service.createTask(acc.Id);
    }
}

trigger AccountTrigger on Account (after insert) {
    AccountTriggerHandler.handleAfterInsert(Trigger.new);
}

@IsTest
public class AccountTriggerTest {

    @TestSetup
    static void seed() { insert new Account(Name='Seed'); }

    @IsTest
    static void shouldCreateTask() {
        Test.startTest();
        insert new Account(Name='Apex Annotations Test');
        Test.stopTest();

        System.assertEquals(1, [SELECT COUNT() FROM Task WHERE Subject='Follow-up']);
    }
}

Testing is another critical part of working with Apex Annotations in Salesforce. Annotations like @IsTest and @TestSetup ensure you maintain code coverage while following best practices for isolation and data setup.

5. @WithSharing / @WithoutSharing / @InheritedSharing

Control the sharing context for classes. @WithSharing respects the running user’s sharing; @WithoutSharing runs in system mode; @InheritedSharing inherits the caller’s context (best default for service classes called by multiple entry points).

Use Case: Respecting vs. Bypassing Sharing

// Respects sharing
public with sharing class CustomerReader {
    public List<Account> listVisible() {
        return [SELECT Id, Name FROM Account LIMIT 200];
    }
}

// Ignores sharing (be careful)
public without sharing class AdminSyncJob {
    public void syncAll() {
        // full access by design; restrict who can schedule this job
        List<Account> allAcc = [SELECT Id, Name FROM Account];
    }
}

// Inherits caller context (safe default)
public inherited sharing class AccountServiceInherited { ... }

Security tip: prefer @InheritedSharing for reusable services and enforce object/field-level checks with Security.stripInaccessible where needed.

6. @ReadOnly

Used on Visualforce controller methods (or some web service contexts) that do not perform DML. Grants a higher limit on queried rows for read-only operations.

Use Case: Large Read-Only Queries

public with sharing class AccountReaderController {
    @ReadOnly
    public static List<Account> getAllAccounts() {
        return [SELECT Id, Name FROM Account ORDER BY Name LIMIT 50000];
    }
}

Note: do not combine @ReadOnly with DML; the request fails if you try to insert/update/delete.

7. @SuppressWarnings

Suppress specific compile-time warnings in limited, intentional cases (e.g., demo code). Avoid using it to hide genuine problems.

Use Case: Ignoring an Unused Variable in a Demo

public with sharing class SuppressWarningsExample {
    @SuppressWarnings('unused')
    public static void example() {
        Integer unusedVar = 0; // intentionally unused for demo
    }
}

8. @Deprecated

Mark classes/methods as obsolete. Keep them callable for backward compatibility while steering new consumers to replacements.

Use Case: Sunsetting an Old API

public with sharing class LegacyBillingService {
    @Deprecated
    public static void oldMethod() {
        // kept for compatibility; schedule removal
    }
}

9. Helpful Extras Often Used With Annotations

  • @RestResource, @HttpGet, @HttpPost: expose REST endpoints with Apex.
  • @TestVisible: make private members visible to tests without changing access modifiers.
  • Queueable / Schedulable (interfaces, not annotations): pair with @Future scenarios when you need chaining/retries or scheduled runs.

Final Considerations

Apex Annotations in Salesforce are powerful — and with power comes responsibility. Prefer safe defaults (@InheritedSharing), keep asynchronous work out of UI transactions when possible, and write focused tests that exercise behavior rather than implementation details. Small, intentional choices around annotations make codebases faster to read and safer to deploy.

When learning Apex Annotations in Salesforce, start with the essentials — @AuraEnabled, @Future, @InvocableMethod, and @IsTest — then expand to advanced concepts like sharing and transaction control. This progressive mastery ensures you stay aligned with modern Salesforce coding standards.

Further learning: see the Salesforce Developer Docs and the Trailhead modules on Apex and Lightning. For architecture topics, check our related post A Complete Guide to the Trigger Handler Pattern in Apex Salesforce.