12.3 Access Modifiers (Public, Internal, Local) & Code Scoping
Key Takeaways
- AL provides three access levels for objects and procedures: Public, Internal, and Local, controlling encapsulation across extension and module boundaries.
- Access = Public is the default for objects, making them accessible to any downstream extension that declares a dependency.
- Access = Internal restricts object or procedure visibility strictly to the declaring extension and explicitly authorized friend extensions declared via internalsVisibleTo in app.json.
- local procedure restricts procedure invocation strictly to the declaring object file, which is the standard best practice pattern for event publisher declarations.
- Setting Extensible = false on tables, pages, or enums prevents downstream third-party extensions from modifying schemas, adding page controls, or injecting unhandled enum ordinals.
12.3 Access Modifiers (Public, Internal, Local) & Code Scoping
As Business Central extension ecosystems scale into multi-app architectures with complex dependencies, developers must enforce strict software architecture boundaries. AL provides robust access modifiers at both the object level and procedure level, as well as manifest configurations for friend extensions, namespace hierarchies, and explicit extensibility locks. For the MB-820 exam, developers must know how to secure internal logic, encapsulate helper objects, prevent breaking changes, and manage API visibility across extensions.
1. Object-Level Access Modifiers (Access Property)
Every AL object (table, codeunit, page, report, query, XMLport, interface, enum, permission set) supports the Access property, which dictates its visibility to other extensions installed on the same tenant or compiled in the same workspace.
codeunit 50110 "Payment Gateway Internal Core"
{
Access = Internal;
Subtype = Normal;
// Codeunit can only be invoked within this extension or declared friend extensions
}
The Three Object Access Levels
| Access Level | Syntax | Scope & Visibility Mechanics |
|---|---|---|
Public | Access = Public; (Default) | The object is fully visible and accessible to any downstream extension that declares a dependency on this app in app.json. This is the default if Access is omitted. |
Internal | Access = Internal; | The object is visible only within the declaring extension package and to explicit "friend extensions" authorized in app.json via internalsVisibleTo. Other third-party extensions cannot reference, extend, or call this object. |
Local | Access = Local; | Restricts visibility strictly to the declaring module or local extension package. (Used primarily in modular system and platform packages). |
Why Use Access = Internal on Objects?
- API Boundary Protection: Expose only clean public facade codeunits while hiding low-level implementation details, database staging tables, and helper algorithms.
- Refactoring Freedom: Internal objects can be refactored, renamed, or redesigned in future releases without causing breaking changes for external ISV consumers.
- Security & Integrity: Prevents third-party extensions from directly manipulating sensitive configuration tables or bypassing validated business pipelines.
2. Friend Extensions & The internalsVisibleTo Manifest
In modular enterprise solutions, an ISV often splits functionality into multiple distinct extensions (e.g., a shared Core Framework extension, a Sales Logistics extension, and an EDI Integration extension). When the Core Framework marks helper codeunits and internal tables as Access = Internal, companion extensions authored by the same publisher need access to those internal objects without exposing them to third-party developers.
This is achieved by declaring Friend Extensions in the app.json manifest of the declaring extension using the internalsVisibleTo array:
{
"id": "e3a45678-1234-4a5b-8c9d-0123456789ab",
"name": "Contoso Core Framework",
"publisher": "Contoso Ltd.",
"version": "2.0.0.0",
"internalsVisibleTo": [
{
"id": "f8b76543-4321-4a5b-8c9d-9876543210fe",
"name": "Contoso Advanced Logistics",
"publisher": "Contoso Ltd."
},
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "Contoso E-Commerce Connector",
"publisher": "Contoso Ltd."
}
]
}
Rules for Friend Extension Resolution
- Exact Manifest Match: The downstream calling extension must have an
id(GUID),name, andpublisherthat strictly match the entry in the declaring extension'sinternalsVisibleTomanifest. - Dependency Declaration: The friend extension must declare an explicit dependency on the parent extension in its own
app.jsondependencieslist. - Transitive Protection: If Extension B is a friend of Extension A, Extension C (which depends on B) does not automatically inherit access to Extension A's internal objects unless Extension C is also explicitly listed in Extension A's
internalsVisibleTo. - Compiler Enforcement: If an unauthorized third-party extension attempts to call an internal object or procedure, the AL compiler rejects the reference with an inaccessibility error — the target is not visible outside its declaring extension.
3. Procedure-Level Access Modifiers
Procedures defined inside AL objects support three distinct access levels:
codeunit 50120 "Inventory Allocation Engine"
{
Access = Public;
// 1. Public Procedure (Default)
procedure CalculateAvailableToPromise(ItemNo: Code[20]): Decimal
begin
// Accessible from any extension that can see this codeunit
exit(ComputeNetAvailability(ItemNo));
end;
// 2. Internal Procedure
internal procedure RecalculateSafetyStock(ItemNo: Code[20])
begin
// Accessible only within this extension and declared friend extensions
UpdateBuffer(ItemNo);
end;
// 3. Local Procedure
local procedure ComputeNetAvailability(ItemNo: Code[20]): Decimal
begin
// Accessible ONLY within this codeunit
exit(100.0);
end;
// Event Publisher pattern (Standard Practice: Local Procedure)
[IntegrationEvent(false, false)]
local procedure OnAfterAllocationComputed(ItemNo: Code[20]; AllocatedQty: Decimal)
begin
end;
}
Procedure Access Level Comparison
| Procedure Modifier | Syntax | Scope & Invocation Rules |
|---|---|---|
| Public | procedure MethodName() | Callable by any object within the same extension, as well as any external extension that has access to the containing object. |
| Internal | internal procedure MethodName() | Callable by any object within the declaring extension package and by authorized friend extensions. External third-party extensions cannot call this procedure even if the containing codeunit is Access = Public. |
| Local | local procedure MethodName() | Callable only from other procedures and triggers within the exact same object file. Completely private and encapsulated. |
Exam Watchout — Event Publishers are
local procedure: In Business Central standard practice, all integration and business event publisher declarations ([IntegrationEvent]/[BusinessEvent]) are declared aslocal procedure. This prevents external code from invoking the publisher procedure directly as a callable method while allowing event subscribers across the system to listen and respond to the event when fired internally.
4. Extensibility Control (Extensible Property)
By default, objects in AL are open to extension by downstream developers (Extensible = true). However, architects can explicitly restrict or disable extensibility on specific objects using the Extensible = false property.
Disabling Extensibility on Tables, Pages, and Enums
// Non-extensible Table: Prevents tableextension objects from adding fields or keys
table 50130 "Encrypted Token Store"
{
Access = Public;
Extensible = false;
DataClassification = CustomerContent;
fields
{
field(1; "Token Key"; Guid) { DataClassification = SystemMetadata; }
field(2; "Secret Payload"; Blob) { DataClassification = CustomerContent; }
}
}
// Non-extensible Enum: Prevents enumextension objects from adding new enum values
enum 50130 "Strict Processing Stage"
{
Extensible = false;
value(0; Initialized) { Caption = 'Initialized'; }
value(1; Validating) { Caption = 'Validating'; }
value(2; Completed) { Caption = 'Completed'; }
value(3; Failed) { Caption = 'Failed'; }
}
Architectural Rationale for Extensible = false
- Preventing SQL Companion Table Locks: In Business Central, each table extension creates a companion SQL table joined at runtime on primary keys. High-throughput telemetry or temporary staging tables subject to hundreds of inserts per second should not allow third-party companion table joins, which could introduce database contention and lock escalation.
- Security & Compliance: Sensitive setup tables (e.g., cryptographic keys, license tokens, audit logs) should prohibit external table extensions from attaching unvalidated triggers or fields.
- Exhaustive State Machine Integrity: Enums used in core posting state machines that require exhaustive
casestatement branching should be markedExtensible = falseso third-party extensions cannot inject unhandled ordinal values that corrupt transactional logic.
5. AL Code Scoping, Namespaces & Packaging Architecture
Modern AL organizes code using Namespaces, aligning Business Central development with modern software engineering standards.
namespace Contoso.Logistics.Allocation;
using Microsoft.Sales.Document;
using Microsoft.Inventory.Item;
using Contoso.Core.Security;
codeunit 50140 "Stock Allocator"
{
Access = Public;
procedure AllocateOrder(SalesHeader: Record "Sales Header")
begin
// Implementation logic referencing imported namespaces
end;
}
- Namespace Declaration: Declared at the top of every
.alfile before object definitions (namespace Company.Product.Module;). - Namespace Resolution: Prevents naming collisions when multiple ISVs create objects with identical names (e.g., two apps defining a table named
"Webhook Log"). usingDirectives: Eliminates the need to fully qualify object names from standard Business Central modules or dependent extensions.
An ISV extension defines an internal helper codeunit with Access = Internal. The ISV needs a second companion extension authored by the same team to call procedures within this internal codeunit. How must the project configuration be set up to permit this access?
A developer creates an enum object in AL to govern a critical financial tax engine. The developer needs to ensure that no third-party ISV extension or per-tenant extension can append additional enum values to this enum. Which property configuration achieves this objective?
A codeunit is defined with Access = Public and contains a procedure declared as: internal procedure RebalanceLedgerEntries(). An external third-party extension references this codeunit. What is the visibility of RebalanceLedgerEntries() when viewed from the external third-party extension?
In Business Central AL development, what is the primary architectural reason why event publisher procedures (decorated with [IntegrationEvent] or [BusinessEvent]) are standardly declared as local procedure rather than procedure (public)?