AL Interfaces with Default Implementations

BC29-Interface-Default-Implementations_1

Introduction

One of the most significant architectural improvements in Business Central 29 is the ability to add default method implementations to AL interfaces. This seemingly simple feature unlocks powerful patterns for managing complex extension architectures, particularly when you’re building platform-level solutions or managing multiple interdependent extensions.

Before BC29, interfaces were immutable once created. Adding a new method meant every implementing class would break until it provided an implementation. In multi-extension ecosystems, this was a maintenance nightmare. BC29 changes this fundamentally.

The Problem: Why This Matters

The Pre-BC29 Dilemma

Consider a scenario common in enterprise solutions: you’ve built a core accounting extension that defines an interface for ledger posting strategies:

// Version 1.0 - Initial Interface
interface ILedgerPostingStrategy
{
    procedure ValidatePostingRules(var GenJnlLine: Record "Gen. Journal Line");
    procedure PostTransaction(var GenJnlLine: Record "Gen. Journal Line"): Boolean;
}

Multiple other extensions implement this interface:

  • StandardLedgerPostingImpl
  • AdvancedAllocationImpl
  • MultiCurrencyImpl
  • Customer A’s custom CustomLedgerPostingImpl

Now, in version 2.0, you want to add support for audit logging. You add a new method to the interface:

// Version 2.0 - With Audit Logging
interface ILedgerPostingStrategy
{
    procedure ValidatePostingRules(var GenJnlLine: Record "Gen. Journal Line");
    procedure PostTransaction(var GenJnlLine: Record "Gen. Journal Line"): Boolean;
    procedure LogAuditTrail(var GenJnlLine: Record "Gen. Journal Line"); // NEW
}

Problem: Every single implementation, including Customer A’s custom code, now breaks with a compiler error. They must add the new method immediately, even if they don’t need audit logging. If they can’t update their code quickly, their extension won’t compile.

This creates a ripple effect through the entire ecosystem:

  • Your customers’ customizations break
  • Third-party extensions break
  • Partner solutions break
  • You can’t release the update without breaking the ecosystem

Why Default Implementations Matter

With BC29’s default implementations, you can evolve your interface safely:

// Version 2.0 - With Default Implementation
interface ILedgerPostingStrategy
{
    procedure ValidatePostingRules(var GenJnlLine: Record "Gen. Journal Line");
    procedure PostTransaction(var GenJnlLine: Record "Gen. Journal Line"): Boolean;
    procedure LogAuditTrail(var GenJnlLine: Record "Gen. Journal Line")
    begin
        // Default: do nothing, but implementing types can override
        // This prevents compilation errors in existing implementations
    end;
}

Now existing implementations continue to work without changes. New implementations (or updated ones) can override the default behavior when needed.

How Default Implementations Work

Basic Syntax

interface IPaymentProcessor
{
    procedure ProcessPayment(var PaymentHeader: Record "Payment Header"): Boolean;
    
    // New method with default implementation
    procedure NotifyPaymentStatus(Status: Text)
    begin
        Message('Payment status: ' + Status);
    end;
    
    // Another with more complex default logic
    procedure CalculateProcessingFee(Amount: Decimal): Decimal
    begin
        // Default: 1% processing fee
        exit(Amount * 0.01);
    end;
}

Implementing Classes Can Override

codeunit 50100 StandardPaymentProcessor implements IPaymentProcessor
{
    procedure ProcessPayment(var PaymentHeader: Record "Payment Header"): Boolean
    begin
        // Implementation
        exit(true);
    end;
    
    // Optional: Override the default behavior
    procedure NotifyPaymentStatus(Status: Text)
    begin
        var
            PaymentLog: Record "Payment Log";
        begin
            PaymentLog.Init();
            PaymentLog.Status := Status;
            PaymentLog.Timestamp := CurrentDateTime;
            PaymentLog.Insert();
        end;
    end;
    
    // Uses default CalculateProcessingFee - no override needed
}

Or Use Defaults As-Is

codeunit 50101 PartnerPaymentProcessor implements IPaymentProcessor
{
    procedure ProcessPayment(var PaymentHeader: Record "Payment Header"): Boolean
    begin
        // Just implements required method
        exit(false);
    end;
    
    // Relies on default NotifyPaymentStatus (no override)
    // Relies on default CalculateProcessingFee (no override)
}

Advanced Pattern: RequiredPending

BC29 introduces the RequiredPending attribute, which allows you to mark methods as “required in the future” while giving developers time to migrate:

interface IReportingService
{
    procedure GenerateReport(ReportID: Integer): Text;
    
    // This method has a default implementation now
    [RequiredPending("2027-04-01", "Real-time report generation is required after April 2027")]
    procedure GenerateRealtimeReport(ReportID: Integer): Text
    begin
        // For now, fall back to standard report generation
        exit(GenerateReport(ReportID));
    end;
}

This gives implementors clear notice that they need to provide an implementation by the deadline. The analyzer will warn them during compilation with your specified deadline and message.

Real-World Scenarios

Scenario 1: API-Level Evolution

You’re maintaining an API that third-party integrations depend on:

interface IExternalAPIHandler
{
    procedure CallExternalAPI(Endpoint: Text; Payload: JsonObject): JsonObject;
    procedure HandleAPIError(ErrorResponse: Text);
}

Version 2.0 needs retry logic:

interface IExternalAPIHandler
{
    procedure CallExternalAPI(Endpoint: Text; Payload: JsonObject): JsonObject;
    procedure HandleAPIError(ErrorResponse: Text);
    
    procedure CallExternalAPIWithRetry(Endpoint: Text; Payload: JsonObject; MaxRetries: Integer): JsonObject
    begin
        var
            RetryCount: Integer;
            Response: JsonObject;
        begin
            repeat
                Response := CallExternalAPI(Endpoint, Payload);
                if not Response.Contains('error') then
                    exit(Response);
                RetryCount += 1;
            until RetryCount >= MaxRetries;
            
            HandleAPIError('Max retries exceeded');
            exit(Response);
        end;
    end;
}

Existing implementations automatically get retry capability without modification.

Scenario 2: Phased Feature Rollout

You’re gradually adding AI capabilities to a reporting interface:

interface IAdvancedReporting
{
    procedure GenerateStandardReport(var ReportBuffer: Record "Report Buffer");
    
    // Phase 1: Default just delegates to standard reporting
    procedure SummarizeWithAI(ReportText: Text): Text
    begin
        exit(ReportText); // Fallback to non-AI for now
    end;
    
    // Phase 2 (BC30): Will be Required
    [RequiredPending("2027-10-01", "AI summarization will be required in BC30")]
    procedure GenerateInsights(var ReportData: Record "Report Data"): Text
    begin
        // Default: very basic insights
        exit('Analysis available after BC30 upgrade');
    end;
}

Your customers can upgrade to BC29 without breaking their implementations, then plan for AI integration before the BC30 deadline.

Scenario 3: Optional Feature Flags

Use default implementations to support optional features gracefully:

interface IDocumentProcessor
{
    procedure ProcessDocument(var Document: Record Document): Boolean;
    
    // Optional: digital signature support
    procedure SignDocument(var Document: Record Document): Boolean
    begin
        // Default: not signed, but processing continues
        Document."Is Signed" := false;
        exit(true);
    end;
    
    // Optional: encryption support
    procedure EncryptDocument(var Document: Record Document): Boolean
    begin
        // Default: no encryption
        exit(true);
    end;
}

Implementations can selectively enable digital signatures and encryption without affecting core document processing.

Migration Strategies

Strategy 1: Gradual Adoption

interface IDataValidator
{
    // Original method - everyone uses this
    procedure ValidateData(var DataRecord: Record "Data Record"): Boolean;
    
    // New in v2.0 - Advanced validation with performance metrics
    procedure ValidateDataWithMetrics(var DataRecord: Record "Data Record"; var ValidationMetrics: Record "Validation Metrics"): Boolean
    begin
        // Default: Just call standard validation, no metrics
        exit(ValidateData(DataRecord));
    end;
}

Implementors can upgrade to use the metrics version on their own timeline. Eventually, you can deprecate the old method.

Strategy 2: Analyzer-Driven Migration

Combine default implementations with custom analyzer rules:

interface IEventHandler
{
    procedure OnBeforeInsert(var Record: Record);
    
    [RequiredPending("2027-06-01", "OnAfterInsert is now available for post-insert logic")]
    procedure OnAfterInsert(var Record: Record)
    begin
        // Default: intentionally empty
    end;
}

Your team can create an analyzer rule that warns developers if they’re still using the old event pattern, guiding them toward the new approach.

Best Practices

1. Provide Meaningful Defaults

// Good: Default has real logic and value
procedure CalculateTax(Amount: Decimal; TaxRate: Decimal): Decimal
begin
    exit(Amount * TaxRate);
end;

// Avoid: Empty default that's not useful
procedure LogTransaction(var Transaction: Record "Transaction")
begin
    // Does nothing - not helpful
end;

2. Document Your Intentions Clearly

interface INotificationService
{
    /// <summary>
    /// Sends a notification through the implementor's channel.
    /// </summary>
    procedure SendNotification(Message: Text; Recipient: Text): Boolean;
    
    /// <summary>
    /// Queues a notification for later delivery. Default implementation
    /// sends immediately, but enterprise implementations should override
    /// to support batch delivery and scheduling.
    /// </summary>
    procedure QueueNotification(Message: Text; Recipient: Text; DeliveryTime: DateTime)
    begin
        // Simple default: deliver immediately, ignore scheduled time
        SendNotification(Message, Recipient);
    end;
}

3. Use RequiredPending for Breaking Changes

interface ILargeDataProcessor
{
    procedure ProcessBatch(var DataRecord: Record "Data Record"): Boolean;
    
    // Not recommended: silently change behavior
    // procedure ProcessBatchParallel(var DataRecord: Record "Data Record"): Boolean
    
    // Better: signal intent to make this required
    [RequiredPending("2027-12-01", "Parallel processing support will be required for performance compliance")]
    procedure ProcessBatchParallel(var DataRecord: Record "Data Record"): Boolean
    begin
        // Default: falls back to sequential processing
        exit(ProcessBatch(DataRecord));
    end;
}

4. Consider Performance Implications

interface IDataSource
{
    procedure GetData(FilterText: Text): List of [Text];
    
    // Default implementation can be expensive - it loads everything then filters
    procedure GetDataWithPaging(FilterText: Text; PageNumber: Integer; PageSize: Integer): List of [Text]
    begin
        var
            AllData: List of [Text];
            StartIndex: Integer;
        begin
            AllData := GetData(FilterText);
            StartIndex := (PageNumber - 1) * PageSize + 1;
            // Very inefficient, but better to have a default than break existing code
            exit(AllData);
        end;
    end;
}

Document that implementors should override this for better performance.

Backward Compatibility Guarantees

When using default implementations properly:

✅ Guaranteed: Existing implementations continue to work without modification
✅ Guaranteed: Code calling the interface continues to work as before
✅ Guaranteed: New implementations can override selectively
✅ Guaranteed: You can add multiple methods with defaults in one update

⚠️ Caution: Default implementations should not change existing method behavior
⚠️ Caution: Don’t remove old methods, mark them obsolete instead
⚠️ Caution: Document when methods with defaults can become required (use RequiredPending)

Comparison: Before vs. After BC29

Before BC29

Customer Extension Version 1 → Implements IPaymentProcessor v1
    ↓
You release IPaymentProcessor v2 with new method
    ↓
COMPILATION ERROR ❌
Customer Extension doesn't compile
Must immediately update their code

After BC29

Customer Extension Version 1 → Implements IPaymentProcessor v1
    ↓
You release IPaymentProcessor v2 with new method + default implementation
    ↓
Customer Extension continues to work ✅
They can optionally override new method on their schedule

Advanced: Combining Interfaces with Defaults

You can create powerful extension hierarchies:

// Base interface
interface IBaseProcessor
{
    procedure Process(var Record: Record): Boolean;
}

// Extended interface adds functionality via defaults
interface IAdvancedProcessor extends IBaseProcessor
{
    procedure PreProcess(var Record: Record)
    begin
        // Default: do nothing before processing
    end;
    
    procedure PostProcess(var Record: Record)
    begin
        // Default: do nothing after processing
    end;
}

Implementors can:

  • Implement just IBaseProcessor (minimal)
  • Implement IAdvancedProcessor and use all defaults (most features without code)
  • Implement IAdvancedProcessor and override selective methods (full customization)

When NOT to Use Default Implementations

  • Abstract methods: If a method is foundational to the interface’s contract, it should NOT have a default
  • Security-critical methods: Never provide a default that could be a security bypass
  • Performance-critical paths: Defaults that have obvious performance issues can mislead implementors
// DON'T do this - security risk
interface IAuthenticationService
{
    procedure AuthenticateUser(Username: Text; Password: Text): Boolean
    begin
        // Default: accept everything - BAD!
        exit(true);
    end;
}

// DO THIS - explicit requirement
interface IAuthenticationService
{
    procedure AuthenticateUser(Username: Text; Password: Text): Boolean;
    // No default - must be implemented
}

Conclusion

Default implementations in AL interfaces represent a maturity milestone for Business Central extensibility. They enable:

  • Ecosystem stability: You can evolve without breaking customer code
  • Phased rollouts: Features can be optional initially, required later
  • Better UX: Implementing classes get sensible defaults, can override as needed
  • Cleaner contracts: Interfaces can express both mandatory and optional behavior

For developers working on platform-level solutions, this is a game-changer for managing complexity in multi-extension architectures.


Next Steps:

  • Review your existing interfaces for methods that could benefit from defaults
  • Plan your BC29 upgrade strategy for extensible solutions
  • Use RequiredPending to communicate future requirements to your ecosystem

Leave a Comment