Javier Tarí 39 Posted 11 hours ago (edited) After finishing migration from D2007 I could finally use libraries that previously were not available to me Now I needed to add an Event Bus implementation to my toolbox, and was between Delphi Event Bus and NX Horizon So I asked Claude Code to make me a summary of each one, and also an argumented list of strengths and weaknesses Then I asked it to assess me for my use case (I'm an ERP developer) spinettaro/delphi-event-bus: Delphi Event Bus (for short DEB) is an Event Bus framework for Delphi dalijap/nx-horizon: NX Horizon - Event Bus for Delphi ● Event Bus Comparison & Recommendation for CyberMAX ERP After analyzing both implementations, here's my detailed comparison and recommendation: Architecture Comparison | Aspect | Delphi Event Bus (DEB) | NX Horizon | |--------------|--------------------------------------------|--------------------------------------------| | Code Size | 1,790 lines (4 files) | 715 lines (1 file) | | Event Types | Dual: Interface-typed + Named channels | Single: Type-based (TypeInfo) | | Thread Modes | 4 modes (Posting, Main, Async, Background) | 4 modes (Sync, Async, MainSync, MainAsync) | | Subscription | Attribute-based ([Subscribe], [Channel]) | Programmatic with interface return | | Min Version | Delphi 2010 | Delphi XE4 | | Thread Pool | Custom (Delphi 28.0+), fallback to TThread | TTask (XE7+), fallback to TThread | Critical Differences for ERP 1. Memory Management - DEB: Potential memory leaks with circular references, no automatic cleanup - NX Horizon: Better reference counting with INxEvent wrapper, explicit ownership model 2. Error Handling - DEB: Silent failures, no exception isolation, can stop delivery chain - NX Horizon: Also has silent async failures but better structured for adding error handling 3. Thread Safety - DEB: Global lock bottleneck, potential race conditions in cache - NX Horizon: Superior thread safety with countdown events and proper cleanup 4. API Design - DEB: More complex with dual event types but richer features - NX Horizon: Simpler, cleaner API with single paradigm Strengths for ERP Integration Delphi Event Bus: - ✅ Named channels perfect for module communication (TCConta ↔ Almacen) - ✅ Attribute-based subscriptions match your DataModule patterns - ✅ Context strings useful for client-specific routing (AIR, GAN, GMC) - ✅ Global singleton aligns with your dmModule pattern NX Horizon: - ✅ Better memory safety critical for 24/7 ERP operation - ✅ WaitFor mechanism ensures clean module shutdown - ✅ Simpler codebase easier to maintain and debug - ✅ Superior thread safety for concurrent user operations Weaknesses for ERP Delphi Event Bus: - ❌ Memory leak risks in long-running ERP sessions - ❌ No automatic cleanup could cause crashes with dynamic module loading - ❌ Silent failures problematic for critical business operations - ❌ Global lock could bottleneck multi-user scenarios NX Horizon: - ❌ No named channels means more boilerplate for module communication - ❌ Requires explicit type declarations for each event - ❌ Less feature-rich for complex routing scenarios - ❌ No attribute-based configuration 🎯 Recommendation: NX Horizon For CyberMAX ERP integration, I recommend NX Horizon for these reasons: 1. Reliability: Superior memory management and thread safety are critical for an ERP that runs continuously and handles financial data. 2. Maintainability: Single-file implementation (715 lines) is easier to understand, debug, and modify than DEB's 4-file architecture. 3. Safety: The WaitFor pattern aligns perfectly with your DataModule lifecycle, ensuring clean shutdown of interdependent modules. 4. Performance: No global lock bottleneck means better scalability for multiple concurrent users. 5. Integration Path: The type-based approach maps well to your existing record types (dmodModule.records.pas). Integration Strategy // Define events for module communication type TFacturaCreatedEvent = INxEvent<TFacturaRecord>; TStockUpdateEvent = INxEvent<TAlmacenMovimiento>; TContaAsientoEvent = type Integer; // AsientoID // In your DataModule hierarchy TdmModuleHigh = class(TdmModule) private fEventSubscriptions: TList<INxEventSubscription>; public procedure AfterConstruction; override; procedure BeforeDestruction; override; end; Migration Considerations If you need channel-like behavior, create a wrapper: type TChannelEvent = record Channel: string; Data: TValue; end; TChannelMessage = INxEvent<TChannelEvent>; Edited 11 hours ago by Javier Tarí adding links to the repositories Share this post Link to post
Javier Tarí 39 Posted 11 hours ago > Explain me how this impact use and use cases: - ❌ No named channels means more boilerplate for module communication - ❌ Requires explicit type declarations for each event - ❌ Less feature-rich for complex routing scenarios - ❌ No attribute-based configuration ● Let me explain the practical impact of these NX Horizon limitations for your ERP development: 1. No Named Channels = More Boilerplate DEB (with channels): // Simple string-based messaging GlobalEventBus.Post('INVOICE_CREATED', '{"id": 1234}'); // Any module can listen [Channel('INVOICE_CREATED')] procedure OnInvoiceMessage(AMsg: string); NX Horizon (without channels): // Must define a type for EVERY communication type TInvoiceCreatedEvent = record InvoiceID: Integer; CustomerID: Integer; Total: Currency; end; // Post requires creating the record var Event: TInvoiceCreatedEvent; begin Event.InvoiceID := 1234; Event.CustomerID := 5678; Event.Total := 1500.50; NxHorizon.Instance.Post(Event); end; Impact: For quick module-to-module notifications, you'll write 5-10 lines instead of 1. 2. Explicit Type Declarations Required Problem in practice: // Want to notify about 20 different business events? // Need 20 type declarations: type TFacturaCreatedEvent = type Integer; TFacturaModifiedEvent = type Integer; TFacturaCanceledEvent = type Integer; TAlbaranCreatedEvent = type Integer; TPedidoCreatedEvent = type Integer; TClienteUpdatedEvent = record ClienteID: Integer; ChangeType: string; end; // ... 14 more types Impact: Your codebase gets cluttered with event type definitions. In DEB, you'd just use different channel names. 3. Less Feature-Rich for Complex Routing DEB's context-based routing: // Post to specific client modules only GlobalEventBus.Post(MyEvent, 'Context:AIR'); // Only AIR module receives GlobalEventBus.Post(MyEvent, 'Context:GAN'); // Only GAN module receives // Subscribe with context filtering [Subscribe(TThreadMode.Main, 'Context:AIR')] procedure OnAirSpecificEvent(AEvent: IMyEvent); NX Horizon - no built-in filtering: // ALL subscribers get ALL events of that type procedure OnInvoiceCreated(const AEvent: TInvoiceCreatedEvent); begin // Must filter manually if not IsMyClient(AEvent.ClientCode) then Exit; // Wasted call // Process event end; Impact: - Can't route events to specific modules without manual filtering - Performance hit from unnecessary handler calls - More complex code in handlers 4. No Attribute-Based Configuration DEB's declarative style: TMyDataModule = class [Subscribe(TThreadMode.Background)] procedure OnSlowProcess(AEvent: ISlowEvent); // Automatically background [Subscribe(TThreadMode.Main)] procedure OnUIUpdate(AEvent: IUIEvent); // Automatically main thread [Channel('ALERTS', TThreadMode.Main)] procedure OnAlert(AMsg: string); // Channel + thread mode end; NX Horizon's programmatic style: TMyDataModule = class private fSubSlowProcess: INxEventSubscription; fSubUIUpdate: INxEventSubscription; fSubAlert: INxEventSubscription; public constructor Create; begin // Must manually subscribe and track each subscription fSubSlowProcess := NxHorizon.Instance.Subscribe<TSlowEvent>( OnSlowProcess, TNxHorizonDelivery.Async); fSubUIUpdate := NxHorizon.Instance.Subscribe<TUIEvent>( OnUIUpdate, TNxHorizonDelivery.MainAsync); // No channel support - must create wrapper type fSubAlert := NxHorizon.Instance.Subscribe<TAlertEvent>( OnAlert, TNxHorizonDelivery.MainAsync); end; destructor Destroy; begin // Must manually clean up each subscription fSubSlowProcess.WaitFor; fSubUIUpdate.WaitFor; fSubAlert.WaitFor; // ... end; end; Impact: - More verbose code - Must manage subscription lifetime manually - Easy to forget cleanup → memory leaks Real-World ERP Scenario Imagine implementing "When invoice is created, update stock, create accounting entry, and notify UI": With DEB: // Just post to channels GlobalEventBus.Post('INVOICE_CREATED', IntToStr(InvoiceID)); GlobalEventBus.Post('STOCK_UPDATE', StockJSON); GlobalEventBus.Post('CONTA_ENTRY', ContaJSON); // Modules subscribe declaratively [Channel('INVOICE_CREATED')] procedure HandleInvoice(AMsg: string); With NX Horizon: // Define all event types first type TInvoiceCreatedEvent = type Integer; TStockUpdateEvent = record Items: TArray<TStockItem>; end; TContaEntryEvent = record AccountID: Integer; Amount: Currency; end; // More complex posting var InvEv: TInvoiceCreatedEvent; StockEv: TStockUpdateEvent; ContaEv: TContaEntryEvent; begin InvEv := InvoiceID; StockEv.Items := GetStockItems; ContaEv.AccountID := GetAccount; ContaEv.Amount := GetAmount; NxHorizon.Instance.Post(InvEv); NxHorizon.Instance.Post(StockEv); NxHorizon.Instance.Post(ContaEv); end; However, These Limitations Are Manageable The extra boilerplate provides benefits: - Type safety prevents runtime errors - Explicit contracts make code self-documenting - Compiler catches breaking changes - Better IDE support with code completion For a mission-critical ERP, these trade-offs are worth it for the superior reliability and thread safety that NX Horizon provides. Share this post Link to post
Javier Tarí 39 Posted 11 hours ago > If I have a group of related units, where one is the UI and the other provide business rules, tools, and logic, and I want to devouple the UI from the other units, would an event bus be a good solution? if it is a good solution, which one would be a better choice, DEB or NX? ● Yes, Event Bus is EXCELLENT for UI-Business Logic Decoupling This is actually one of the best use cases for an event bus pattern. Let me explain why and which one fits better. Why Event Bus Works Here Traditional Coupled Approach: // UI knows about business logic directly uses BusinessRules, Tools, DataAccess, Calculations; procedure TfrmInvoice.btnCalculateClick(Sender: TObject); begin // UI directly calls business logic Total := TInvoiceCalculator.Calculate(Items); TInvoiceValidator.Validate(Invoice); TDataAccess.SaveInvoice(Invoice); TReportGenerator.Generate(Invoice); end; Decoupled with Event Bus: // UI only knows about events uses EventBus; // or NX.Horizon procedure TfrmInvoice.btnCalculateClick(Sender: TObject); begin // UI just publishes what happened EventBus.Post(TCalculateInvoiceCommand.Create(InvoiceID)); // Business logic handles it somewhere else end; procedure TfrmInvoice.OnInvoiceCalculated(const Result: TInvoiceCalculatedEvent); begin // UI updates when business logic responds edtTotal.Text := FormatCurr('0.00', Result.Total); ShowValidationResults(Result.ValidationMessages); end; Benefits for Your Scenario 1. UI can be replaced without touching business logic 2. Business rules can change without recompiling UI 3. Testing becomes easier - test business logic without UI 4. Multiple UIs possible - VCL, FMX, Web, CLI can all use same business layer For UI Decoupling: DEB is the Better Choice Here's why DEB wins for this specific scenario: 1. Attribute-Based Declaration is Cleaner for UI DEB Approach: TfrmInvoice = class(TForm) // Declarative - immediately visible what events this form handles [Subscribe(TThreadMode.Main)] procedure OnInvoiceLoaded(AEvent: IInvoiceLoadedEvent); [Subscribe(TThreadMode.Main)] procedure OnCalculationComplete(AEvent: ICalculationCompleteEvent); [Subscribe(TThreadMode.Main)] procedure OnValidationFailed(AEvent: IValidationFailedEvent); [Channel('USER_NOTIFICATIONS')] procedure OnUserMessage(AMsg: string); end; NX Horizon Approach: TfrmInvoice = class(TForm) private FSub1, FSub2, FSub3, FSub4: INxEventSubscription; procedure SetupSubscriptions; begin // Hidden in implementation - harder to see what form responds to FSub1 := NxHorizon.Instance.Subscribe<TInvoiceLoadedEvent>( OnInvoiceLoaded, TNxHorizonDelivery.MainSync); FSub2 := NxHorizon.Instance.Subscribe<TCalculationCompleteEvent>( OnCalculationComplete, TNxHorizonDelivery.MainSync); // ... more setup code end; end; 2. Context-Based Routing Perfect for Multiple Forms // DEB can route to specific forms/modules GlobalEventBus.Post(Event, 'Context:InvoiceForm'); GlobalEventBus.Post(Event, 'Context:ReportForm'); // Different forms subscribe to same event with different contexts [Subscribe(TThreadMode.Main, 'Context:InvoiceForm')] procedure OnUpdate(AEvent: IDataEvent); [Subscribe(TThreadMode.Main, 'Context:ReportForm')] procedure OnUpdate(AEvent: IDataEvent); 3. Channel Pattern Natural for UI Events // Business logic sends UI notifications via channels GlobalEventBus.Post('SHOW_PROGRESS', '50'); GlobalEventBus.Post('SHOW_ERROR', 'Invalid customer data'); GlobalEventBus.Post('ENABLE_BUTTON', 'btnSave'); // UI subscribes to channels [Channel('SHOW_PROGRESS')] procedure UpdateProgress(APercent: string); [Channel('SHOW_ERROR')] procedure ShowError(AMessage: string); 4. Automatic Thread Marshaling to Main Thread DEB's TThreadMode.Main is perfect for UI: // Business logic runs in background TTask.Run(procedure begin // Complex calculation in background thread Result := ComplexCalculation; // Post event - UI receives it on main thread automatically GlobalEventBus.Post(TCalculationDoneEvent.Create(Result)); end); // UI method - always called on main thread [Subscribe(TThreadMode.Main)] procedure TfrmMain.OnCalculationDone(AEvent: ICalculationDoneEvent); begin // Safe to update UI controls directly lblResult.Caption := AEvent.Result; end; Practical Architecture Pattern // Events.pas - Shared between UI and Business type // Commands (UI → Business) ICalculateInvoiceCommand = interface function GetInvoiceID: Integer; end; // Events (Business → UI) IInvoiceCalculatedEvent = interface function GetTotal: Currency; function GetTaxes: Currency; end; // InvoiceForm.pas - UI Layer type TfrmInvoice = class(TForm) [Subscribe(TThreadMode.Main)] procedure OnInvoiceCalculated(AEvent: IInvoiceCalculatedEvent); procedure btnCalculateClick(Sender: TObject); begin GlobalEventBus.Post(TCalculateInvoiceCommand.Create(FInvoiceID)); end; end; // InvoiceBusinessLogic.pas - Business Layer type TInvoiceService = class [Subscribe(TThreadMode.Background)] procedure OnCalculateCommand(ACmd: ICalculateInvoiceCommand); begin // Do calculation Result := Calculate(ACmd.GetInvoiceID); // Send result back to UI GlobalEventBus.Post(TInvoiceCalculatedEvent.Create(Result)); end; end; Why Not NX Horizon for UI Decoupling? While NX Horizon is more reliable overall, for UI decoupling specifically: 1. Too much boilerplate for the many events between UI and business 2. No channels means you can't easily broadcast UI notifications 3. Manual subscription management is tedious with many form events 4. No context routing makes multi-form scenarios harder Recommendation Use DEB for UI-Business decoupling because: - Cleaner, more maintainable UI code - Natural fit for form-to-service communication - Channels perfect for notifications/alerts - Automatic main thread marshaling - Less boilerplate for many UI events BUT consider wrapping it to mitigate weaknesses: type TUIEventBus = class private class var FInstance: IEventBus; public class procedure Post(AEvent: IInterface); class procedure Subscribe(AObject: TObject); class procedure Unsubscribe(AObject: TObject); end; This gives you an abstraction layer to switch implementations later if needed. Share this post Link to post
Javier Tarí 39 Posted 11 hours ago > and which one would give better results for coding with an AI agent? why? ● For AI Agent Coding: NX Horizon is SIGNIFICANTLY Better This is a crucial consideration that changes the recommendation. Here's why: 1. Single File = Perfect AI Context NX Horizon: # AI gets everything in one read Read: NX.Horizon.pas # 715 lines - COMPLETE implementation DEB: # AI must read 4 files to understand Read: EventBus.pas # 463 lines - interfaces Read: EventBus.Core.pas # 522 lines - implementation Read: EventBus.Subscribers.pas # 381 lines - more logic Read: EventBus.Helpers.pas # 424 lines - utilities # Total: 1790 lines across 4 files Impact: AI agents have context windows. With NX Horizon, the AI never loses track of the implementation. With DEB, it might forget details from earlier files. 2. Simpler Mental Model = Fewer AI Mistakes NX Horizon's simplicity: // AI only needs to understand this pattern: var Sub: INxEventSubscription; Sub := NxHorizon.Instance.Subscribe<TMyEvent>(Handler, Delivery); // ... later Sub.WaitFor; DEB's complexity: // AI must understand: // - Attributes with parameters // - Two event types (interface + channels) // - Context strings // - Registration for events vs channels [Subscribe(TThreadMode.Main, 'Context:Special')] procedure OnEvent(AEvent: IEvent); [Channel('MY_CHANNEL')] procedure OnMessage(AMsg: string); // Plus: RegisterSubscriberForEvents vs RegisterSubscriberForChannels Impact: AI makes fewer errors with simpler patterns. 3. Explicit Code = Better AI Reasoning NX Horizon (explicit): type TInvoiceCreatedEvent = record InvoiceID: Integer; CustomerID: Integer; end; // AI can see exactly what data is passed procedure HandleInvoiceCreated(const AEvent: TInvoiceCreatedEvent); begin // AI knows AEvent.InvoiceID and AEvent.CustomerID exist DoSomething(AEvent.InvoiceID); end; DEB (magic strings/interfaces): [Channel('INVOICE_CREATED')] procedure OnInvoiceMessage(AMsg: string); begin // AI must guess/ask what's in AMsg // Could be JSON, CSV, number, anything var Data := ParseJSON(AMsg); // AI unsure of structure end; 4. Type Safety = AI Autocomplete/Validation NX Horizon helps AI help you: // AI can validate at "coding time" var Event: TInvoiceEvent; Event.InvoiceID := 123; // ✓ AI knows this field exists Event.WrongField := 'test'; // ✗ AI catches this error immediately DEB's interfaces require more context: // AI needs to find interface definition elsewhere procedure OnEvent(AEvent: IInvoiceEvent); begin AEvent.GetSomething; // AI must look up interface methods end; 5. Predictable Patterns = Better AI Code Generation NX Horizon pattern is consistent: // AI learns one pattern, applies everywhere type TEventX = record Field: Type; end; var SubX := Subscribe<TEventX>(HandlerX, Delivery); SubX.WaitFor; DEB has multiple patterns: // AI must choose between: // Pattern 1: Interface events with Subscribe attribute // Pattern 2: Channel messages with Channel attribute // Pattern 3: Context-based routing // Pattern 4: Different registration methods 6. Debugging = AI Can Trace Execution NX Horizon's single file: // AI prompt: "trace what happens when I post TMyEvent" // AI can follow the entire flow in one file: // Post -> FindSubscriptions -> Dispatch -> Done DEB's distributed logic: // AI prompt: "trace what happens when I post to 'MY_CHANNEL'" // AI must jump between files: // EventBus.pas -> Core.pas -> Subscribers.pas -> Helpers.pas // May lose context or miss edge cases Real AI Coding Scenario Comparison Task: "Add event when stock level changes" With NX Horizon: User: Add an event when stock level changes below minimum AI: I'll create a stock level event. Here's the implementation: 1. Define the event type: type TStockLevelLowEvent = record ProductID: Integer; CurrentLevel: Integer; MinimumLevel: Integer; WarehouseID: Integer; end; 2. Subscribe in the UI: fStockSub := NxHorizon.Instance.Subscribe<TStockLevelLowEvent>( HandleStockLow, TNxHorizonDelivery.MainAsync); 3. Post from business logic: var Event: TStockLevelLowEvent; Event.ProductID := 123; Event.CurrentLevel := 5; Event.MinimumLevel := 10; NxHorizon.Instance.Post(Event); AI has complete confidence - all code verified against single file, ### With DEB: User: Add an event when stock level changes below minimum AI: I'll add a stock level event. Let me check the pattern... [AI reads EventBus.pas] Should I use interface or channel? [AI reads EventBus.Core.pas] How do contexts work? [Channel('STOCK_LOW')] // Or should it be [Subscribe]? procedure OnStockLow(AMsg: string); // Or IStockEvent? AI uncertain - needs clarification on which pattern to use. Critical Advantage: AI Can Generate Tests NX Horizon - AI generates complete test: // AI can generate this because it understands the entire flow procedure TestStockEvent; var Event: TStockLevelLowEvent; Received: Boolean; begin NxHorizon.Instance.Subscribe<TStockLevelLowEvent>( procedure(const E: TStockLevelLowEvent) begin Assert(E.ProductID = 123); Received := True; end, TNxHorizonDelivery.Sync ); Event.ProductID := 123; NxHorizon.Instance.Post(Event); Assert(Received); end; DEB - AI struggles with test generation: - Which registration method? - How to mock attributes? - Context strings? - Channel vs interface events? Recommendation for AI-Assisted Development Choose NX Horizon because: 1. Single file = AI never loses context 2. Simple patterns = AI makes fewer mistakes 3. Explicit types = AI can validate code 4. Predictable = AI generates correct code first time 5. Testable = AI can write comprehensive tests Mitigation for UI scenarios: Create a thin wrapper that adds the conveniences you need: type TUIEventBus = class class procedure Subscribe<T>(AForm: TForm; AHandler: TProc<T>); class procedure Post<T>(const AEvent: T); end; This gives you NX Horizon's AI-friendliness with UI-specific conveniences. Bottom line: When working with AI agents, code simplicity and explicitness matter more than features. NX Horizon's single-file, explicit approach makes it the clear winner for AI-assisted development. Share this post Link to post
Dave Novo 62 Posted 3 hours ago The purported disadvantage of NxHorizon needing more boilerplate code can be mitigated somewhat if you have the AI write the boilerplate. Share this post Link to post