Apex Classes
An Apex class is a reusable container for Apex code, holding properties, methods, constructors, inner classes, and constants.
Definition
An Apex class is a reusable container for Apex code, holding properties, methods, constructors, inner classes, and constants. It is the building block of every nontrivial Apex codebase. Triggers delegate to classes for the actual logic. Lightning components invoke class methods to fetch and update data. REST endpoints expose class methods to external systems. Scheduled and batch jobs implement specific class interfaces. Everything beyond the smallest one-liner lives inside a class.
Apex classes look syntactically similar to Java or C# classes. They support inheritance with the extends keyword, interfaces with implements, generics for collections (List<Account>, Map<Id, String>), and a familiar set of access modifiers (public, private, global, protected). The runtime adds Salesforce-specific behavior: sharing context declaration (with sharing, without sharing, inherited sharing), governor limits, automatic JSON serialization for REST endpoints, and integration with the platform's caching, queueing, and async execution frameworks. Most Apex engineering work is class design: deciding what each class should know, what methods it should expose, and how classes compose into a maintainable system.
How Apex classes organize business logic on Salesforce
Access modifiers and the four visibility levels
Apex supports four access levels: private (visible only within the class), protected (visible to the class and its subclasses), public (visible within the namespace), and global (visible across namespaces, including from external packages). Global is necessary for methods exposed in managed packages and for REST endpoint declarations, but it is also irrevocable once a managed package is uploaded. Use public unless global is required. Default access is private; explicit modifiers document intent and pass code review.
Sharing context: with sharing, without sharing, inherited
Every Apex class should declare its sharing context. with sharing respects the running user's record access; SOQL queries return only records the user can see. without sharing ignores sharing rules; SOQL returns everything. inherited sharing uses the calling context's sharing, which is the right default for utility classes invoked from other classes. Default (unannotated) classes behave as without sharing in many trigger contexts, which is a security pitfall. Always declare explicitly.
Static versus instance methods and state
Static methods belong to the class and run without an instance. They are the right choice for stateless utility logic (StringHelper.titleCase, DateHelper.daysBetween). Instance methods belong to an object created from the class and can hold state in instance fields. Class design usually starts with static methods and adds instance-based design when state matters. Static state persists across the transaction within a single execution context, which is useful for caching but dangerous if assumptions about lifetime are wrong.
Interfaces and the standard ones the platform exposes
Apex interfaces declare method signatures that implementing classes must provide. The platform exposes several standard interfaces: Database.Batchable<T> for batch jobs, Queueable for queued async work, Schedulable for cron-based execution, Comparable for custom sort orders, Database.AllowsCallouts for batch jobs that make HTTP requests. Implementing these interfaces is how you plug Apex classes into the platform's async and integration frameworks. Custom interfaces let you build domain abstractions inside your own codebase.
The Trigger Handler pattern
The most common class pattern in production Apex is the Trigger Handler. A thin trigger file delegates to a handler class that holds all the actual logic, organized by event-context method (handleBeforeInsert, handleAfterUpdate). The handler class can extend a base TriggerHandler framework that manages recursion, bypass logic, and context dispatch. This pattern keeps trigger files searchable, handler classes testable in isolation, and the codebase maintainable across years and team turnover.
REST endpoints and the @RestResource annotation
Apex classes can expose REST endpoints by adding the @RestResource(urlMapping='/MyService/*') annotation and declaring methods with @HttpGet, @HttpPost, @HttpPut, @HttpDelete annotations. The platform automatically routes incoming HTTP requests to the matching method, serializes parameters from the request body, and returns JSON responses. This is the lightweight way to expose custom integration endpoints; for complex APIs, prefer the standard REST framework with proper documentation, versioning, and rate limiting.
Testing classes with @isTest and assertions
Apex test classes are marked with @isTest at the class or method level. Tests run in their own data context (no production data is visible by default) and have separate governor limit pools via Test.startTest and Test.stopTest. The System.Assert class (System.Assert.areEqual, System.Assert.isTrue) replaces the older System.assertEquals API and produces clearer failure messages. Comprehensive testing means covering positive cases, negative cases, bulk cases, and boundary conditions, with meaningful assertions on the expected end state.
How to write a maintainable Apex Class
Writing a single Apex class is straightforward once you know the syntax. Writing one that integrates cleanly with the rest of the codebase, tests well, and survives three years of evolution requires deliberate design. Follow the conventions: explicit sharing context, focused single-responsibility methods, separated test classes with meaningful assertions, and a clear naming pattern.
- Decide the class purpose and naming
Pick a name that describes what the class does. AccountTriggerHandler for a trigger delegate. OpportunityService for business logic on Opportunity. AccountRepository for data access. The naming convention drives the codebase organization and onboarding speed.
- Create the file with explicit access and sharing modifiers
public with sharing class AccountService for most cases. global with sharing only when the class must be visible across namespaces. Without explicit sharing declaration, the class runs without sharing in trigger contexts, which is a security risk.
- Define focused methods with single responsibilities
Each public method should do one thing. Methods longer than 50 lines or with multiple responsibilities should be refactored into smaller pieces. Private helper methods inside the class hold reusable internal logic.
- Add inline documentation for non-obvious behavior
ApexDoc comments above public methods explain parameters and return values. Inline comments mark assumptions and business rules that future readers cannot infer from the code. Skip comments that just restate what the code obviously does.
- Create the matching test class
Separate file: AccountServiceTest.cls. Use @isTest at the class level. Use @testSetup to build common data once. Write test methods for each public method, covering positive cases, negative cases, and bulk cases.
- Run tests and verify coverage
Run from Developer Console, VS Code, or SFDX. Confirm coverage exceeds the 75 percent minimum. Review each uncovered line and decide if a test is needed. Most production codebases target 85 to 90 percent.
- Deploy via change set, metadata API, or SFDX
Bundle the class and test class together. Run a quick local test cycle before deployment. Use a CI/CD pipeline for serious projects so every change exercises the full test suite.
- Monitor in production for exceptions
Build a central error logging pattern. A custom Error_Log__c object with a static logException method catches caught exceptions for debugging. The Apex Exception Email is too sparse for production troubleshooting.
private, protected, public, or global. Default is private; declare explicitly for code review clarity.
with sharing, without sharing, or inherited sharing. Must be declared explicitly to prevent unintended access escalation.
Pinned per class. Keep on a recent version to access new features and avoid deprecated behaviors.
- Classes without explicit sharing declarations default to without-sharing semantics in trigger contexts, which is a security risk. Always declare with sharing, inherited sharing, or without sharing explicitly.
- The global access modifier is irrevocable for classes inside managed packages. Use public unless the class genuinely needs cross-namespace visibility, because downgrading from global later breaks subscribers.
- Static state persists across the transaction within a single execution context but resets between transactions. Caching with static fields works but assumptions about lifetime can produce stale data bugs.
- Test classes do not see production data by default. Use @isTest(SeeAllData=true) only when truly necessary, and prefer @testSetup for building isolated test data because SeeAllData ties tests to production data shape.
- API version pinning means old classes may execute differently from new ones. Upgrade API versions periodically during planned refactoring, not in production hotfixes.
Trust & references
Cross-checked against the following references.
- Apex ClassesSalesforce Developer
- Using the with sharing or without sharing KeywordsSalesforce Developer
- InterfacesSalesforce Developer
Straight from the source - Salesforce's reference material on Apex Classes.
- Apex Classes ReferenceSalesforce Developer
- Class Access ModifiersSalesforce Developer
- Testing Apex ClassesSalesforce Developer
Hands-on resources to go deeper on Apex Classes.
About the Author
Dipojjal Chakrabarti is a B2C Solution Architect with 29 Salesforce certifications and over 13 years in the Salesforce ecosystem. He runs salesforcedictionary.com to help admins, developers, architects, and cert/interview candidates sharpen their fundamentals. More about Dipojjal.
Test your knowledge
Q1. What is a Governor Limit in the context of Apex Classes?
Q2. Where would a developer typically work with Apex Classes?
Q3. What is required before deploying Apex Classes-related code to production?
Discussion
Loading discussion…