Custom Controller
A Custom Controller in Salesforce is an Apex class that supplies all the data and behavior for a Visualforce page on its own, without sitting on top of the standard controller framework.
Definition
A Custom Controller in Salesforce is an Apex class that supplies all the data and behavior for a Visualforce page on its own, without sitting on top of the standard controller framework. The page names the class in its controller attribute. From then on, every property the page reads and every action it calls has to exist as a method on that class. Custom controllers hand the developer full control over queries, business logic, and navigation, in exchange for writing more code than a standard controller needs.
You reach for a custom controller when a standard controller cannot do the job. Standard controllers are bound to one object and limited to the CRUD operations that object already supports. A page that queries across several objects, runs multi-step logic, calls an external service, or renders a screen that does not map to a single record needs its own Apex class behind it. One important detail: a custom controller runs in system mode by default, so it ignores the user's object permissions, field-level security, and sharing unless you add the with sharing keyword.
How a custom controller drives a Visualforce page
Declaring the class on the page
A custom controller is a public Apex class. The Visualforce page connects to it through the controller attribute on the apex:page tag, for example controller="MyPageController". Once that link exists, the page can read any public property on the class using merge syntax like {!myProperty}, and it can call any public method that takes no arguments through a commandButton or commandLink action. The class needs a no-argument constructor, because the framework instantiates it that way when the page first loads. Everything the page shows or does now flows through this one class. There are no inherited record methods, no automatic save, and no default object binding the way a standard controller provides. That blank slate is the point. You decide what the page queries, how it shapes the data, and what each button does. The trade is verbosity: a record edit page that a standard controller handles in one line of markup might take fifty lines of Apex when you build it from scratch as a custom controller.
Getter and setter methods
Getter methods feed data to the page. When the page renders an expression like {!contact}, the framework calls the matching getContact method on the controller and uses what it returns. Getters fire during rendering, and a single getter can be called more than once per request, so they should stay cheap and avoid running a SOQL query every time. The common pattern is to query once in the constructor or in an action method, store the result in a member variable, and have the getter just return that variable. Setter methods move data the other way. When a user types into an inputText bound to {!searchTerm} and submits the form, the framework calls setSearchTerm with the entered value before any action runs. You rarely write setters by hand now, because an Apex property with get; set; generates both accessors for you. The naming is strict: a property named foo maps to getFoo and setFoo, and the page expression {!foo} will not resolve if those accessors are missing or not public.
Action methods and PageReference
Action methods handle what happens when a user clicks something. A commandButton with action="{!save}" calls the public save method on the controller. Action methods return a PageReference, and that return value tells the framework where to send the user next. Return null to stay on the current page and re-render it with whatever state the method changed. This is the most common choice for search, filter, and add-row interactions, because it keeps the user in place. Return a new PageReference object to redirect, for example new PageReference('/' + recordId) to send the user to a record, or Page.AnotherVfPage to jump to a different Visualforce page by name. Set setRedirect(true) on the PageReference when you want a full browser redirect that clears view state, instead of a server-side forward that preserves it. Action methods are also where you run DML, because they execute inside a postback request that is allowed to change data. A getter that tried to run an insert would fail, since the rendering phase is meant to be read-only.
The page lifecycle: get requests versus postbacks
A custom controller is created fresh for every request and thrown away at the end. There is no instance that survives between clicks. What carries state across requests is the view state, not the object. The order of execution splits into two cases. On the first load, a get request, the constructor runs, then expression getters fire as the page renders. No action or setter methods run, because the user has not submitted anything yet. On a postback, the request that fires when a user clicks a commandButton, the framework rebuilds the controller from view state, calls setters to apply the submitted input, runs the action method tied to the button, and then calls getters again to render the response. Understanding this split explains many bugs. If a value resets every time a button is clicked, it is usually because it was never stored in a member variable that view state preserves, so it gets recomputed from scratch on each postback. Salesforce documents this order separately for get requests and postback requests.
System mode, sharing, and security
By default a custom controller runs in system mode. The current user's object permissions, field-level security, and record sharing are not enforced, so the class can read and write data the running user could never touch through the standard UI. That is powerful and dangerous. To make the controller respect the user's sharing, declare the class as public with sharing class MyController. Sharing keywords control record visibility, but they do not enforce object permissions or field-level security on their own. For those, you check access in code with Schema describe calls or Security.stripInaccessible, or you bind fields through standard components that enforce FLS. Treat a custom controller as code that runs with elevated rights until you prove otherwise. A search page that queries Cases with a plain SOQL statement and no with sharing will happily return Cases the user is not allowed to see, then display them on the page. This is one of the most common security review findings on legacy Visualforce, so set the sharing keyword deliberately on every controller.
View state and governor limits
A custom controller runs as Apex, so it lives under the standard governor limits: 100 SOQL queries per synchronous transaction, 150 DML statements, and a 6 MB heap. The limit unique to Visualforce is the view state, capped at 170 KB. Every non-transient member variable on the controller is serialized into the view state and shipped to the browser on each render, then sent back on the next postback. A controller that holds a large list of records, or a fat wrapper object, can blow past 170 KB and throw a view state error. The fix is the transient keyword. Mark any variable that does not need to survive to the next request as transient, and it stays out of view state entirely. Static variables are also excluded. The ReadOnly annotation on an action or page raises the SOQL row limit from 50,000 to one million for that request, which helps report-style pages that scan a lot of data, though it forbids DML during that request.
Testing a custom controller
A custom controller is tested like any Apex class, plus a few Visualforce-specific steps. The pattern is to create test data, instantiate the controller with new MyController(), set its public properties to mimic the input a user would type, call the action methods, and assert on the resulting state. Action methods return a PageReference, so you can assert that the redirect target is what you expect, or that it returned null. When the page or controller reads URL parameters, set them before constructing the controller using ApexPages.currentPage().getParameters().put('id', someId), or push a page with Test.setCurrentPage(Page.MyPage). Add a getter-only state check by reading the public property after the action runs. Cover the failure paths too: a save that hits a validation rule should add an ApexPages message, and your test can assert that ApexPages.hasMessages() is true. Aim past the 75 percent coverage floor and assert on behavior, not just line execution, because a custom controller with green coverage and no assertions still ships bugs to production pages.
How to build a custom controller
You build a custom controller by writing an Apex class and pointing a Visualforce page at it. Here is the minimal path from empty class to a working page, with the choices that matter at each step.
- Create the Apex class
In Setup, go to Apex Classes and click New, or create the class in your IDE. Declare it public, add the with sharing keyword unless you have a documented reason not to, and give it a no-argument constructor. Run any initial queries in that constructor and store results in member variables.
- Expose data with properties
Add public properties using get; set; for anything the page reads or writes. A property named searchTerm becomes {!searchTerm} on the page. Keep getters cheap; do not run SOQL inside a getter that the page calls during rendering.
- Write action methods
Add public methods that return PageReference for each button or link. Return null to re-render in place, or a new PageReference to redirect. Run all DML inside these action methods, never inside getters.
- Bind the page to the class
Create the Visualforce page and set controller="YourClassName" on the apex:page tag. Reference your properties with merge expressions and wire buttons with action="{!yourMethod}". Wrap inputs in apex:form so postbacks work.
- Write the test class
Create an Apex test that instantiates the controller, sets properties, sets any URL parameters with ApexPages.currentPage().getParameters().put, calls the action methods, and asserts on the returned PageReference and the controller state.
The class must be public (or global). The controller attribute cannot bind to a private class.
The framework instantiates a custom controller with a parameterless constructor, so one must exist (the implicit default counts if you write no constructor at all).
The apex:page tag must set controller="ClassName" to link the page to the custom controller. This is what makes every expression resolve against your class.
Anything the page references, getters, setters, and action methods, must be public. Private members are invisible to the page and the expression will fail to resolve.
- Without with sharing, the controller runs in system mode and can surface records the user is not allowed to see.
- A getter that runs SOQL is called multiple times per render and will burn query limits fast; query once and cache in a variable.
- Large non-transient member variables inflate view state toward the 170 KB cap; mark anything not needed next request as transient.
- DML in a getter throws an error because rendering is read-only; move all inserts, updates, and deletes into action methods.
Prefer this walkthrough as its own page? How to Custom Controller in Salesforce, step by step
Trust & references
Cross-checked against the following references.
Straight from the source - Salesforce's reference material on Custom Controller.
Hands-on resources to go deeper on Custom Controller.
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. On a Visualforce page, how is a Custom Controller attached so its methods become reachable?
Q2. What does a Custom Controller action method most commonly return to re-render the same page with updated state?
Q3. Which limit most directly punishes a Custom Controller that exposes large objects as public properties?
Discussion
Loading discussion…