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.
Last updated: August 2026

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 LevelSyntaxScope & Visibility Mechanics
PublicAccess = 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.
InternalAccess = 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.
LocalAccess = 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

  1. Exact Manifest Match: The downstream calling extension must have an id (GUID), name, and publisher that strictly match the entry in the declaring extension's internalsVisibleTo manifest.
  2. Dependency Declaration: The friend extension must declare an explicit dependency on the parent extension in its own app.json dependencies list.
  3. 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.
  4. 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.
Loading diagram...
AL Access Modifiers & Friend Extension Architecture

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 ModifierSyntaxScope & Invocation Rules
Publicprocedure MethodName()Callable by any object within the same extension, as well as any external extension that has access to the containing object.
Internalinternal 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.
Locallocal 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 as local 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

  1. 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.
  2. Security & Compliance: Sensitive setup tables (e.g., cryptographic keys, license tokens, audit logs) should prohibit external table extensions from attaching unvalidated triggers or fields.
  3. Exhaustive State Machine Integrity: Enums used in core posting state machines that require exhaustive case statement branching should be marked Extensible = false so 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 .al file 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").
  • using Directives: Eliminates the need to fully qualify object names from standard Business Central modules or dependent extensions.
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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)?

A
B
C
D