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:
StandardLedgerPostingImplAdvancedAllocationImplMultiCurrencyImpl- 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
IAdvancedProcessorand use all defaults (most features without code) - Implement
IAdvancedProcessorand 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
RequiredPendingto communicate future requirements to your ecosystem
