Eclipse Plugin Developer Guide
Plugins let you extend Eclipse platform behaviour at runtime without modifying platform source code. A plugin is a plain JAR uploaded through the Plugin API. The platform hot-loads it, wires CDI injection into it, and calls its handler methods when the relevant events occur.
There are three broad categories of plugin:
| Category | Mechanism | Use cases |
|---|---|---|
| Injection-point plugins | @OnNamedInjectionPoint annotated methods | Intercept events, enrich data, override behaviour at named hooks |
| Implementation plugins | Extend a Pluggable* base class | Provide a custom payment gateway, withdrawal gateway, loan gateway, remittance gateway, payment processor, withdrawal processor, fraud handler, or wallet type |
| Listener plugins | Extend a Pluggable*Listener class | React to service lifecycle events (user creation, payment completion, loan updates, document uploads, etc.) |
1. Core Annotations
@PluginDescriptor (required, class-level)
@PluginDescriptor (required, class-level)Every plugin JAR must contain exactly one class annotated with @PluginDescriptor. This is how the platform identifies and names the plugin.
@PluginDescriptor(
name = "my-plugin", // unique plugin name (required)
version = "1.0", // version string (optional, default "1.0")
description = "Does something useful" // human-readable description (optional)
)
public class MyPluginInfo { }The name field must be unique across all loaded plugins. Uploading a JAR with an existing name is treated as an upgrade.
@OnNamedInjectionPoint (method-level)
@OnNamedInjectionPoint (method-level)Marks a method as a handler for a named platform event.
@OnNamedInjectionPoint(
injectionPoint = "wallet.postWalletTransfer", // event name (required)
priority = 10 // higher = runs first (optional, default 0)
)
public void afterTransfer(@Named("from") IWallet from, @Named("to") IWallet to) { ... }The framework resolves method parameters by type or by name (see section 3). Multiple methods in the same plugin, or across multiple plugins, can handle the same injection point. They run in descending priority order.
2. Plugin Lifecycle and Management
Uploading a plugin
Plugins are managed via the Plugin REST API. To deploy a plugin:
POST /eclipse-conductor/rest/v1/plugins
Content-Type: multipart/form-data
jar=<jarBytes> (required)
status=ON|OFF (optional, defaults to OFF)
isGlobal=true (optional, applies to all tenants)
tenantIds=1,2,3 (optional, specific tenants)
On first upload the platform scans the JAR, discovers all @OnNamedInjectionPoint methods, and auto-populates a config JSON:
{
"injectionPoints": {
"com.example.MyPlugin#afterTransfer": true,
"com.example.MyPlugin#onWalletCreate": true
}
}Each key is ClassName#methodName. Set the value to false to disable a specific handler without disabling the whole plugin.
Plugin status
| Status | Effect |
|---|---|
ON | Plugin is active and handlers are called |
OFF | Plugin is loaded but no handlers are called |
DELETED | Plugin is unloaded and garbage collected |
PUT /eclipse-conductor/rest/v1/plugins/{pluginId}/status
Body: "ON"
Updating a plugin
Re-upload a JAR with the same @PluginDescriptor name. The platform merges the existing config with the new JAR's injection points — handlers that no longer exist in the new JAR are automatically removed from the config; new handlers are added with enabled: true; existing handlers keep their current enabled state.
PUT /eclipse-conductor/rest/v1/plugins/{pluginId}
Body: { "jar": "<base64>" }
Tenant mapping
A plugin can be global (runs for all tenants) or restricted to specific tenant IDs:
PUT /eclipse-conductor/rest/v1/plugins/{pluginId}/mappings
Body: { "global": false, "mappings": { "tenantIds": [42, 99] } }
3. Injection-Point Plugins
Writing a handler
@PluginDescriptor(name = "transfer-audit", version = "1.0")
@Singleton
public class TransferAuditPlugin {
@OnNamedInjectionPoint(injectionPoint = "wallet.postWalletTransfer", priority = 5)
public void auditTransfer(
@Named("from") IWallet from,
@Named("to") IWallet to,
@Named("history") List<WalletHistory> history) {
// 'from' and 'to' are the same IWallet type, so @Named is required to distinguish them
log.info("Transfer: {} → {} ({} entries)", from.getWalletId(), to.getWalletId(), history.size());
}
}CDI injection in plugins
Classes annotated with @Singleton, @ApplicationScoped, @RequestScoped, or @Dependent get CDI wiring. The platform injects the following services automatically:
| Type | Notes |
|---|---|
CallAsUserJsonRestClient | REST client that calls other services as the current user |
CallAsSystemJsonRestClient | REST client that calls other services as the system |
RequestContext | Current request context (tenant ID, user, session, etc.) |
EntityManager | JPA entity manager |
Add them a method parameters to receive them:
@Singleton
public class MyPlugin {
@OnNamedInjectionPoint(injectionPoint = "domainEvent")
public void onEvent(DomainEvent event, RequestContext ctx, EntityManager em) {
log.info("Tenant {} fired event {}", ctx.getTenantId(), event);
}
}Parameter resolution rules
The framework resolves each method parameter in this order:
- If the parameter is annotated with
@Named("x"), it looks for aNamedParamwhose name matches"x"in thectxvarargs passed tofireInjectionPoint. - Otherwise it resolves by type — exact match first, then assignability, then the global CDI context.
When the same type appears more than once at an injection point all such arguments are wrapped in NamedParam by the platform. Your method must use @Named to tell the framework which one you want, otherwise it throws.
// Correct — two IWallet args must use @Named
public void handle(@Named("from") IWallet from, @Named("to") IWallet to) { ... }
// Wrong — ambiguous, will throw at runtime
public void handle(IWallet from, IWallet to) { ... }Return values
If a handler returns a non-null value, it is collected by the caller. fireInjectionPoint returns a List<Object> of all non-null return values from all handlers, in priority order. The platform uses this for injection points like convertCurrency and sendTextSms where a plugin is expected to produce a result.
4. All Supported Injection Points
Wallet events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
wallet.preWalletCreate | Before a new wallet is persisted | Wallet newWallet | — |
wallet.postWalletCreate | After a new wallet is persisted | Wallet newWallet | — |
wallet.onWalletUpdate | When wallet fields are updated | Wallet wallet | — |
wallet.preReservation | Before a reservation is created | BigDecimal amount, String sessionId, ZonedDateTime expiryDate, String description, Boolean force | sessionId, description |
wallet.preWalletTransfer | Before a wallet transfer is executed | IWallet from, IWallet to, Boolean bulk, Boolean scheduled | from, to, bulk, scheduled |
wallet.postWalletTransfer | After a wallet transfer completes | IWallet from, IWallet to, List<WalletHistory> history, Boolean bulk, Boolean scheduled | from, to, history, bulk, scheduled |
wallet.augmentTransfer | To augment/split a transfer before it executes | IWallet from, IWallet to, Boolean bulk, Boolean scheduled | from, to, bulk, scheduled |
wallet.doSMSForFailedTransfer | When a transfer fails and the platform wants to send an SMS | Transfer transfer, IWallet from, IWallet to, String failedReason | from, to |
wallet.onTransactionFailure | When a transaction fails at the ledger level | Long walletId, Long otherWalletId, BigDecimal amount, String currency, String sessionId | walletId, otherWalletId, currency, sessionId |
wallet.postWalletTypeCreate | After a new wallet type is created | WalletType walletType | — |
wallet.onWalletTypeUpdate | When a wallet type is updated | WalletType walletType, DbWalletType existing | — |
Exchange rate
| Injection Point | When fired | Parameters | @Named keys | Expected return type |
|---|---|---|---|---|
convertCurrency | When a spot exchange rate is needed | String baseCurrency, String toCurrency | baseCurrency, toCurrency | ExchangeRate |
convertCurrencyHistoric | When a historic exchange rate is needed | String baseCurrency, String toCurrency, Instant date | baseCurrency, toCurrency, date | ExchangeRate |
If a plugin returns an
ExchangeRate, the platform uses it. If no plugin handles the injection point, the built-in rate provider is used.
SMS
| Injection Point | When fired | Parameters | @Named keys | Expected return type |
|---|---|---|---|---|
sendTextSms | When the platform needs to send an SMS | String cardBin, String to, String message, String senderId, Object rawObject | cardBin, to, message, senderId, rawObject | String (provider message ID) |
HTTP request / response interception
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
conductorRequest | On every inbound HTTP request to Eclipse Conductor (before auth and routing) | ContainerRequestContext requestContext | — |
conductorResponse | On every outbound HTTP response from Eclipse Conductor | ContainerRequestContext requestContext, ContainerResponseContext responseContext | — |
Use
JaxRsUtils.replyImmediately(rq, response)inside aconductorRequesthandler to short-circuit the request and return a custom response immediately.
Domain events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
domainEvent | When any domain event is published via UK.Events | DomainEvent event | — |
Exceptions and alerts
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
exception | When an exception is processed by the platform exception handler | Throwable t, List<? extends ExceptionData> edList | — |
alert | When an alert is sent via UK.Alerts | ExceptionSeverity severity, String channelName, String message, ZonedDateTime alertDate, Throwable t, String traceId | channelName, message, traceId |
Payment events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
payment.preCreatePayment | Before a new payment is persisted | NewPayment newPayment | — |
payment.postCreatePayment | After a new payment is persisted | Payment payment | — |
payment.preCreateWithdrawal | Before a new withdrawal is persisted | Withdrawal newWithdrawal | — |
payment.postCreateWithdrawal | After a new withdrawal is persisted | Withdrawal withdrawal | — |
payment.postPaymentComplete | After a payment reaches a terminal completed state | Payment payment | — |
payment.postWithdrawalComplete | After a withdrawal reaches a terminal completed state | Withdrawal withdrawal | — |
payment.prePaymentAcquiring | Before payment acquiring is attempted | DbPayment payment | — |
Loan events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
loan.preCreateLoan | Before a new loan is persisted | NewLoan newLoan | — |
loan.postCreateLoan | After a new loan is persisted | Loan loan | — |
loan.preLoanUpdate | Before a loan is updated | Loan loan, DbLoan existingLoan | — |
loan.postLoanUpdate | After a loan is updated | Loan loan | — |
Remittance events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
remittance.preCreateRemittance | Before a new remittance is persisted | NewRemittance newRemittance | — |
remittance.postCreateRemittance | After a new remittance is persisted | Remittance remittance | — |
remittance.preRemittanceUpdate | Before a remittance is updated | Remittance remittance, DbRemittance existingRemittance | — |
remittance.postRemittanceUpdate | After a remittance is updated | Remittance remittance | — |
remittance.postRemittanceComplete | After a remittance reaches a terminal completed state | Remittance remittance | — |
remittance.preRemittanceAcquiring | Before remittance acquiring is attempted | Remittance remittance | — |
User and organisation events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
user.onUserCreate | Before a new user is created | User newUser | — |
user.onUserCreated | After a new user is persisted | User user | — |
user.onUserUpdate | Before a user is updated | User changes, DbUser existing | — |
user.onUserUpdated | After a user is updated | User user | — |
user.onUserDelete | When a user is deleted | User user | — |
user.onOrganisationCreate | Before a new organisation is created | Organisation organisation | — |
user.onOrganisationCreated | After a new organisation is persisted | Organisation organisation | — |
user.onOrganisationUpdate | Before an organisation is updated | Organisation changes, DbOrganisation existing | — |
user.onOrganisationUpdated | After an organisation is updated | Organisation organisation | — |
user.onOrganisationDelete | When an organisation is deleted | Organisation organisation | — |
user.onUserIdentityCreate | After a user identity is created | UserIdentity userIdentity (secretOrKey and info stripped) | — |
user.onUserIdentityDelete | When a user identity is deleted | UserIdentity userIdentity | — |
user.preAuth | Before authentication is attempted | AuthCredentials authData | — |
user.onAuthSuccess | After a successful authentication | AuthCredentials authData (password stripped), User user | — |
user.onAuthFailure | After a failed authentication | AuthCredentials authData (password stripped), DbUser dbUser | — |
user.onDocumentCreate | After a document is created | Document document (binary data stripped) | — |
user.onDocumentUpdate | After a document is updated | Document document (binary data stripped) | — |
user.onDocumentDelete | When a document is deleted | Document document (binary data stripped) | — |
user.onAddressCreate | After an address is created | Address address | — |
user.onAddressUpdate | Before an address is updated | Address address, DbAddress existing | — |
user.onAddressUpdated | After an address is updated | Address address | — |
user.onAddressDelete | When an address is deleted | Address address | — |
user.onUserContactCreate | After a user contact is created | UserContact userContact | — |
user.onUserContactUpdate | Before a user contact is updated | UserContact userContact, DbUserContact existing | — |
user.onUserContactUpdated | After a user contact is updated | UserContact userContact | — |
user.onUserContactDelete | When a user contact is deleted | UserContact userContact | — |
user.onUserPositionCreate | After a user position record is created | UserPosition userPosition | — |
user.onUserPositionDelete | When a user position record is deleted | UserPosition userPosition | — |
user.onUserRoleCreate | After a user role is assigned | UserRole userRole | — |
user.onUserRoleDelete | When a user role is removed | UserRole userRole | — |
user.onUserRatified | After a user KYC ratification result is stored | RatifyResult result | — |
user.onOrganisationRatified | After an organisation KYC ratification result is stored | RatifyResult result | — |
user.onManualRatify | When a manual ratify result is submitted | String ratifyResultData | — |
user.postRatifyBatchCompletion | After a ratify batch job completes | String queueName, RatifyBatchType type, Long batchId, String checks | — |
user.onPasswordUpdate | When a user password is changed | UserIdentityPasswordChange passwordChange | — |
user.onUserIdentityPasswordChangeInit | When a password-change flow is initiated | PasswordChangeInit initiatePasswordChange | — |
Credential fields (
password,secretOrKey,info) are always stripped before these events fire so that plugin code cannot read or log credentials.
Attachment events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
attachment.onGetAttachments | When a list of attachments is retrieved | List<Attachment> attachments, String fields | — |
Property events
| Injection Point | When fired | Parameters | @Named keys |
|---|---|---|---|
property.postPropertyCreate | After a new property is created | Property property | — |
property.onPropertyUpdate | When a property is updated | Property property | — |
property.onPropertyDelete | When a property is deleted | Property property | — |
property.onPropertyGet | When properties are retrieved | List<DbProperty> properties | — |
Propertyis a global platform resource with no tenant concept. All property injection points fire withtenantId = null.
5. Implementation Plugins
Implementation plugins provide a complete custom implementation of a platform interface. The plugin class extends a Pluggable* base class and is registered by class name in configuration. When the platform needs an instance of that interface it calls UK.Runtime.getBean(className), which returns the plugin's CDI-managed instance.
5.1 Fraud Event Handler
Use case: Provide a custom fraud decision engine. The platform calls handle() for each fraud event and uses the returned result.
Base class: com.ukheshe.services.fraud.publish.pluggable.PluggableFraudEventPublishHandler
Interface method:
FraudEventPublishResponse handle(FraudEventPublishEvent fraudPublishEvent)Plugin example:
@PluginDescriptor(name = "my-fraud-engine", version = "1.0")
@Singleton
public class MyFraudPlugin extends PluggableFraudEventPublishHandler {
@Override
public FraudEventPublishResponse handle(FraudEventPublishEvent event) {
FraudEventPublishResponse response = new FraudEventPublishResponse();
// Inspect event.getMessage(), event.getMessageType(), event.getTenantId() etc.
response.setFraudResult(FraudEventPublishResult.NOT_FRAUD);
return response;
}
}Configuration property:
fraud.event.handlers.<handlerType> = com.example.MyFraudPlugin
Where <handlerType> matches the handlerType field on the FraudEventPublishEvent sent by the transaction pipeline (e.g. MYENGINE).
Field mapping utilities:
FraudEventPublishHandler provides static helpers for reading tenant-specific field mappings from properties:
Map<String, String> requestFields = FraudEventPublishHandler.getRequestConfig(event);
Map<String, String> responseFields = FraudEventPublishHandler.getResponseConfig(event);Property key format:
fraud.event.handler.<handlerType>.field-mappings.request.<messageTypeRegex> # global
fraud.event.handler.<handlerType>.field-mappings.request.<messageTypeRegex>.<tenantId> # tenant override
5.2 Wallet Type
Use case: Provide a custom wallet type with overridden balance calculation, custom charging logic, or bespoke limit behaviour. The platform instantiates one wallet object per wallet access.
Base class: com.ukheshe.services.wallet.pluggable.PluggableWallet
Key base class method:
protected void setSuperClass(BiDirectionalDelegatingWallet superclass)Call setSuperClass in @PostConstruct, passing an injected BiDirectionalDelegatingLimitCheckingWallet. This wires the bidirectional delegation that makes super.* calls work correctly through managed CDI beans (see note below).
Plugin example:
@PluginDescriptor(name = "high-balance-wallet", version = "1.0")
@Dependent
public class HighBalanceWallet extends PluggableWallet {
@PostConstruct
public void init() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingLimitCheckingWallet.class));
}
@Override
public BigDecimal getCurrentBalance() {
// override — calls the real implementation via super
return super.getCurrentBalance().add(BONUS);
}
}Configuration:
Set the implementationClass field on the wallet type DB record to the fully qualified class name of your plugin class (e.g. com.example.HighBalanceWallet). The WalletFactory uses this field to look up and instantiate the correct implementation for each wallet.
Why
setSuperClass? Quarkus generates CDI proxy classes at build time. A plugin loaded at runtime cannot directly extend a managed bean; if it tried, callingsuper.method()would hit an unmanaged instance where all@Injectfields are null.PluggableWallet+BiDirectionalDelegatingLimitCheckingWalletsolve this: the plugin extends the plainPluggableWalletPOJO, which delegates to the managed bean via a bidirectional reference. When the managed bean callsthis.method()internally, it is redirected to the plugin subclass so standard Java polymorphism works as expected.
5.3 Payment Gateway
Use case: Implement a custom payment gateway (e.g. a new card acquirer, mobile money provider, or voucher system).
Base class: com.ukheshe.services.payment.gateway.pluggable.PluggablePaymentGateway
Interface: com.ukheshe.services.payment.gateway.IPaymentGateway
Bidirectional delegate interface: com.ukheshe.services.payment.gateway.pluggable.BiDirectionalDelegatingPaymentGateway
Call setSuperClass(BiDirectionalDelegatingPaymentGateway) in @PostConstruct to wire bidirectional delegation. The concrete bean to inject is BasePaymentGateway, which implements this interface.
Key methods the plugin must implement:
| Method | Purpose |
|---|---|
void init(DbPayment dbPayment) | Called first with the payment record; call super.init() to delegate to the base gateway |
void initiatePayment(Object gatewaySpecificData) | Initiate payment with the external provider |
void paymentModified(Object gatewaySpecificData) | Handle an async update to an existing payment |
void sync() | Poll / reconcile payment state |
void cancel() | Cancel a pending payment |
void reverse(Object gatewaySpecificData) | Reverse a completed payment |
void expire() | Mark a timed-out payment as expired |
void refund(DbRefund refund) | Process a refund |
boolean supportsPreProcessing() | Whether the gateway supports pre-auth holds |
boolean supportsReverse() | Whether the gateway supports reversals |
boolean supportsHold() | Whether the gateway supports authorisation holds |
List<PaymentProof> getPaymentProofs(DbPayment, String fields) | Return payment proof documents |
Plugin example:
@PluginDescriptor(name = "my-payment-gateway", version = "1.0")
@ApplicationScoped
public class MyPaymentGateway extends PluggablePaymentGateway {
@PostConstruct
public void setup() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingPaymentGateway.class));
}
@Override
public void initiatePayment(Object data) {
// call external provider
}
@Override
public boolean supportsReverse() { return false; }
// ... implement remaining interface methods
}Registration: The plugin class must be registered in the payment gateway factory. Contact the platform team to add a new gateway type string mapping to your class, or use an existing type string with a property override.
5.4 Withdrawal Gateway
Use case: Implement a custom withdrawal gateway (e.g. a new bank EFT rail, mobile wallet disbursement, or ATM network).
Base class: com.ukheshe.services.payment.gateway.pluggable.PluggableWithdrawalGateway
Interface: com.ukheshe.services.payment.gateway.IWithdrawalGateway
Bidirectional delegate interface: com.ukheshe.services.payment.gateway.pluggable.BiDirectionalDelegatingWithdrawalGateway
Call setSuperClass(BiDirectionalDelegatingWithdrawalGateway) in @PostConstruct. The concrete bean to inject is BaseWithdrawalGateway, which implements this interface.
Key methods:
| Method | Purpose |
|---|---|
void init(DbWithdrawal dbWithdrawal) | Called first with the withdrawal record |
void processWithdrawal() | Send the withdrawal to the external provider |
void withdrawalModified(Object gatewaySpecificData) | Handle an async update |
WithdrawalQuote withdrawalQuote(double amount) | Return a fee/FX quote for the amount |
void cancel() | Cancel a pending withdrawal |
void expireWithdrawal() | Mark a timed-out withdrawal expired |
void sync() | Poll / reconcile withdrawal state |
boolean supportsPreProcessing() | Whether the gateway supports pre-processing |
WithdrawalLimits getWithdrawalLimits(Long tenantId, String currency, String gateway, WithdrawalType, WithdrawalSubType) | Return configured limits |
Plugin example:
@PluginDescriptor(name = "my-withdrawal-gateway", version = "1.0")
@ApplicationScoped
public class MyWithdrawalGateway extends PluggableWithdrawalGateway {
@PostConstruct
public void setup() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingWithdrawalGateway.class));
}
@Override
public void processWithdrawal() {
DbWithdrawal wd = getDbWithdrawal();
// call external provider
}
// ... implement remaining methods
}Registration: Same as payment gateway — register the type string in the withdrawal gateway factory.
5.5 Loan Gateway
Use case: Implement a custom loan gateway to connect to an external lending provider (credit bureau, BNPL platform, or in-house loan engine).
Base class: com.ukheshe.services.loan.gateway.pluggable.PluggableLoanGateway
Interface: com.ukheshe.services.loan.gateway.ILoanGateway
Bidirectional delegate interface: com.ukheshe.services.loan.gateway.pluggable.BiDirectionalDelegatingLoanGateway
Concrete delegate bean: com.ukheshe.services.loan.gateway.pluggable.BiDirectionalDelegatingBaseLoanGateway (@Dependent )
Call setSuperClass(BiDirectionalDelegatingLoanGateway) in @PostConstruct, injecting the BiDirectionalDelegatingBaseLoanGateway bean.
Key methods:
| Method | Purpose | Base impl? |
|---|---|---|
void init(DbLoan dbLoan) | Initialise the gateway with the loan record | Yes — call super.init() |
void initiateLoan(Object gatewaySpecificData) | Submit the loan to the external provider | No — must override |
LoanOptIn loanOptIn(NewLoanOptIn newLoanOptIn) | Enrol a customer into a loan product | No — must override |
void loanOptOut(NewLoanOptOut loanOptOut) | Remove a customer from a loan product | Yes |
void loanRePayment(DbLoan existingLoan, NewLoanRePayment loanRePayment) | Record a loan repayment | Yes |
void updateLoan(Object gatewaySpecificData) | Handle an async update to an existing loan | No — must override |
LoanLimit loanLimit(String customerId, String productId) | Return current loan limits for a customer | No — must override |
Loan getLoan(DbLoan dbLoan) | Fetch current loan state from the provider | No — must override |
List<Loan> getLoans(DbLoan filter, int offset, int limit) | List loans matching a filter | No — must override |
boolean supportsPreProcessing() | Whether the gateway supports pre-processing | Yes |
boolean supportsReverse() | Whether the gateway supports reversals | Yes |
List<LoanTransaction> getLoanTransactions(long tenantId, long loanId, Long walletId) | Return transaction history for a loan | Yes |
Plugin example:
@PluginDescriptor(name = "my-loan-gateway", version = "1.0")
@ApplicationScoped
public class MyLoanGateway extends PluggableLoanGateway {
@Inject BiDirectionalDelegatingBaseLoanGateway baseGateway;
@PostConstruct
public void setup() {
setSuperClass(baseGateway);
}
@Override
public void initiateLoan(Object data) {
// call external lending provider
}
@Override
public LoanOptIn loanOptIn(NewLoanOptIn newLoanOptIn) {
// enrol customer
return new LoanOptIn();
}
// ... implement remaining no-base-impl methods
}Registration: Contact the platform team to register the gateway class for the relevant loan type.
5.6 Remittance Gateway
Use case: Implement a custom remittance gateway (cross-border money transfer, mobile money payout, or FX provider).
Base class: com.ukheshe.services.remittance.gateway.pluggable.PluggableRemittanceGateway
Interface: com.ukheshe.services.remittance.gateway.IRemittanceGateway
Bidirectional delegate interface: com.ukheshe.services.remittance.gateway.pluggable.BiDirectionalDelegatingRemittanceGateway
Concrete delegate bean: com.ukheshe.services.remittance.gateway.pluggable.BiDirectionalDelegatingBaseRemittanceGateway (`@Dependent)
Call setSuperClass(BiDirectionalDelegatingRemittanceGateway) in @PostConstruct, injecting the BiDirectionalDelegatingBaseRemittanceGateway bean.
Key methods:
| Method | Purpose | Base impl? |
|---|---|---|
void init(DbRemittance dbRemittance) | Initialise the gateway with the remittance record | Yes — call super.init() |
void initiateRemittance(Object gatewaySpecificData) | Submit the remittance to the external provider | No — must override |
void getRemittance(DbRemittance dbRemittance) | Fetch current remittance state from the provider | No — must override |
void remittanceModified(Object gatewaySpecificData) | Handle an async update to an existing remittance | Yes |
void cancel() | Cancel a pending remittance | Yes |
void reverse(Object gatewaySpecificData) | Reverse a completed remittance | Yes |
void userEnrollment(DbRemittanceUserEnrollment enrollment) | Enrol a user with the remittance provider | Yes |
void getUserEnrollmentStatus(DbRemittanceUserEnrollment enrollment) | Check a user's enrolment status | Yes |
void updateUserEnrollment(DbRemittanceUserEnrollment enrollment) | Update a user's enrolment record | Yes |
boolean supportsPreProcessing() | Whether the gateway supports pre-processing | Yes |
boolean supportsReverse() | Whether the gateway supports reversals | Yes |
void sync() | Poll / reconcile remittance state | Yes |
DbRemittance getDbRemittance() | Return the current DB record | Yes |
RemittanceQuickQuoteResponse getQuickQuotes(RemittanceQuickQuoteRequest request) | Return indicative quotes | Yes |
CompletionStage<List<ConfirmationQuotesResponse>> getConfirmationQuotes(DbRemittance dbRemittance) | Return confirmation quotes | Yes |
Plugin example:
@PluginDescriptor(name = "my-remittance-gateway", version = "1.0")
@ApplicationScoped
public class MyRemittanceGateway extends PluggableRemittanceGateway {
@Inject BiDirectionalDelegatingBaseRemittanceGateway baseGateway;
@PostConstruct
public void setup() {
setSuperClass(baseGateway);
}
@Override
public void initiateRemittance(Object data) {
// call external remittance provider
}
@Override
public void getRemittance(DbRemittance dbRemittance) {
// fetch current state
}
// ... override other methods as needed
}Registration: Contact the platform team to register the gateway class for the relevant remittance type.
5.7 Payment Processor
Use case: Customise how a payment is enriched before creation, how its result is interpreted, or how completion is attempted. Processors sit between the Conductor API layer and the payment service.
Base class: com.ukheshe.eclipse.conductor.service.payments.PluggablePaymentProcessor
Interface: com.ukheshe.eclipse.conductor.service.payments.IPaymentProcessor
Key methods:
| Method | Purpose |
|---|---|
void enrichForCreation(NewEclipsePayment, NewPayment) | Populate payment-service fields from the conductor request |
void enrichWithUpdates(UpdatedPaymentTransactionData, Payment) | Apply async update data to the payment record |
PaymentResult enrichResult(PaymentResult, Payment) | Post-process the result returned to the API caller |
Payment attemptPaymentCompletion(long tenantId, Payment) | Try to complete a deferred payment |
Reservation enrichForRefund(Payment, EclipsePaymentRefund, NewPaymentRefund) | Prepare a refund reservation |
void validatePaymentReceivingWallet(NewPayment) | Validate that the destination wallet can receive this payment |
Plugin example:
@PluginDescriptor(name = "my-payment-processor", version = "1.0")
@ApplicationScoped
public class MyPaymentProcessor extends PluggablePaymentProcessor {
@PostConstruct
public void setup() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingMyBaseProcessor.class));
}
@Override
public void enrichForCreation(NewEclipsePayment eclipsePayment, NewPayment newPayment) {
super.enrichForCreation(eclipsePayment, newPayment); // delegates to base
// additional enrichment
}
}Registration: The processor factory selects a processor based on payment type. Contact the platform team to wire a new payment type to your plugin class.
5.8 Withdrawal Processor
Use case: Customise enrichment, fee calculation, or limit enforcement for a withdrawal flow.
Base class: com.ukheshe.eclipse.conductor.service.withdrawals.PluggableWithdrawalProcessor
Interface: com.ukheshe.eclipse.conductor.service.withdrawals.IWithdrawalProcessor
Key methods:
| Method | Purpose |
|---|---|
Long enrichForCreation(Wallet, NewEclipseWalletWithdrawal, Withdrawal, Long destinationWalletId) | Enrich the withdrawal before creation; return destination wallet ID |
void enrichWithUpdates(Wallet, UpdatedWalletWithdrawal, Withdrawal) | Apply an async update to the withdrawal record |
WalletWithdrawalResult enrichResult(Wallet, WalletWithdrawalResult, Withdrawal) | Post-process the result |
Fee getWithdrawalFee(long tenantId, Wallet, String type, BigDecimal amount) | Return the fee for this withdrawal |
WithdrawalLimits getWithdrawalLimit(long tenantId, Wallet, String type) | Return applicable limits |
Plugin example:
@PluginDescriptor(name = "my-withdrawal-processor", version = "1.0")
@ApplicationScoped
public class MyWithdrawalProcessor extends PluggableWithdrawalProcessor {
@PostConstruct
public void setup() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingMyBaseWithdrawalProcessor.class));
}
@Override
public Fee getWithdrawalFee(long tenantId, Wallet wallet, String type, BigDecimal amount) {
// custom fee logic
return new Fee(amount.multiply(BigDecimal.valueOf(0.01)));
}
}Registration: Contact the platform team to register your processor class for the relevant withdrawal type.
6. Listener Plugins
Listener plugins let you react to service lifecycle events across any microservice. Each service exposes a Pluggable*Listener class that you extend. Overriding a method gives you a pre-post hook around that lifecycle event, with access to full CDI injection.
Listener chain
Every service has a four-layer listener chain:
SilentXxxListener (no-op base — defines the interface)
└── PublishingXxxListener (publishes domain events via UK.Events)
└── PluggableXxxListener (fires injection points via UK.Plugins)
└── EclipseXxxListener (platform overrides; extend this one)
Your plugin should extend EclipseXxxListener (or a service-specific subclass of it provided by the platform). When you call super.method(...), the call propagates up through PluggableXxxListener (firing the injection point) and then PublishingXxxListener (publishing the domain event). Both always run unless you intentionally skip the super call.
Available Pluggable*Listener classes
Pluggable*Listener classes| Service | Class |
|---|---|
| Wallet | com.ukheshe.services.wallet.listener.PluggableWalletListener |
| Payment | com.ukheshe.services.payment.listener.PluggablePaymentListener |
| Loan | com.ukheshe.services.loan.listener.PluggableLoanListener |
| Remittance | com.ukheshe.services.remittance.listener.PluggableRemittanceListener |
| User | com.ukheshe.services.user.listener.PluggableUserListener |
| Attachment | com.ukheshe.services.attachment.listener.PluggableAttachmentListener |
| Property | com.ukheshe.services.property.listener.PluggablePropertyListener |
Plugin example
@PluginDescriptor(name = "payment-audit", version = "1.0")
@ApplicationScoped
public class PaymentAuditListener extends EclipsePaymentListener {
@Inject AuditService audit;
@Override
public void postCreatePayment(Payment payment) {
audit.record(payment.getTenantId(), "payment.create", payment.getPaymentId());
super.postCreatePayment(payment); // fires injection point + publishes domain event
}
@Override
public void postPaymentComplete(Payment payment) {
audit.record(payment.getTenantId(), "payment.complete", payment.getPaymentId());
super.postPaymentComplete(payment);
}
}Registration: Contact the platform team to configure the listener class for the relevant service. The platform instantiates one listener per service via CDI and calls it for every lifecycle event.
7. Building a Plugin JAR
A plugin is a regular JAR with no main class. It must:
- Declare one
@PluginDescriptor-annotated class — used for identification and versioning. - Implement handler methods or extend a
Pluggable*base class (or both). - Not bundle platform classes — classes from
com.ukheshe.*are available at runtime via the parent class loader and must not be included in the plugin JAR.
The plugin-sdk.jar at the root of the repository contains the platform API classes needed for compilation. Reference it as a provided-scope dependency.
<dependency>
<groupId>com.ukheshe</groupId>
<artifactId>plugin-sdk</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>Build a fat JAR that includes your own classes and any third-party libraries not already on the platform classpath.
8. Quick Reference
Minimal injection-point plugin
@PluginDescriptor(name = "my-plugin", version = "1.0", description = "Example plugin")
public class MyPluginDescriptor { }
@Singleton
public class MyHandlers {
@OnNamedInjectionPoint(injectionPoint = "wallet.postWalletCreate", priority = 1)
public void onWalletCreated(Wallet wallet) {
// runs after every wallet is created
}
}Minimal fraud handler plugin
@PluginDescriptor(name = "my-fraud-plugin", version = "1.0")
public class MyFraudDescriptor { }
@Singleton
public class MyFraudHandler extends PluggableFraudEventPublishHandler {
@Override
public FraudEventPublishResponse handle(FraudEventPublishEvent event) {
FraudEventPublishResponse res = new FraudEventPublishResponse();
res.setFraudResult(FraudEventPublishResult.NOT_FRAUD);
return res;
}
}Property: fraud.event.handlers.MYENGINE = com.example.MyFraudHandler
Minimal wallet type plugin
@PluginDescriptor(name = "my-wallet", version = "1.0")
public class MyWalletDescriptor { }
@Dependent
public class MyWallet extends PluggableWallet {
@PostConstruct
public void init() {
setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingLimitCheckingWallet.class));
}
@Override
public BigDecimal getCurrentBalance() {
return super.getCurrentBalance(); // add custom logic here
}
}Wallet type DB record: implementationClass = com.example.MyWallet
Minimal loan gateway plugin
@PluginDescriptor(name = "my-loan-gateway", version = "1.0")
public class MyLoanDescriptor { }
@ApplicationScoped
public class MyLoanGateway extends PluggableLoanGateway {
@Inject BiDirectionalDelegatingBaseLoanGateway baseGateway;
@PostConstruct
public void init() {
setSuperClass(baseGateway);
}
@Override
public void initiateLoan(Object data) { /* call provider */ }
@Override
public LoanOptIn loanOptIn(NewLoanOptIn req) { return new LoanOptIn(); }
@Override
public void updateLoan(Object data) { }
@Override
public LoanLimit loanLimit(String customerId, String productId) { return null; }
@Override
public Loan getLoan(DbLoan dbLoan) { return null; }
@Override
public List<Loan> getLoans(DbLoan filter, int offset, int limit) { return List.of(); }
}Minimal listener plugin
@PluginDescriptor(name = "my-listener", version = "1.0")
public class MyListenerDescriptor { }
@ApplicationScoped
public class MyListener extends EclipseUserListener {
@Override
public void onUserCreated(User user) {
// react to new user
super.onUserCreated(user); // always call super to fire injection point + domain event
}
}Updated about 6 hours ago
