Revisiting the possibility of DCI in Java

13 views
Skip to first unread message

Alexandru Balmus

unread,
Oct 2, 2025, 4:30:08 AM (11 days ago) Oct 2
to object-composition
Hi everyone,

Ever since my last (failed) attempt to implement DCI in Java, I've continued researching for possible approaches and want to share my results.

As I understand, one possible (acceptable) way to implement DCI is using extension methods. While Java does not natively support this feature, there are at least two projects (Lombok and Manifold) that implement compiler annotation processors which enhance the Java language with additional useful features.

Lombok is very popular in the Java world nowadays, being used for generating boilerplate code (getters, setters, toString, builders...) at compile time based on specific annotations in the code.
This project also has support for extension methods, albeit in experimental status:

 https://projectlombok.org/features/experimental/ExtensionMethod 

Manifold is an alternative option that has full support for extension methods:

 
However this project is not that widely used.

I've chosen to experiment with Lombok due to it's popularity, thus many projects could introduce DCI using this approach right away.

So, how does it work?

Assuming the classic bank account example, with a simple object of type Account, then any public static method that takes as first parameter an object of type Account is a candidate for an extension method.

So, if I have a class MoneyTransferContext that contains a nested class called Account_Source (that will represent a role) as follows:

    static class Account_Source
    {
        public static void transfer(Account thiz, Account destination, Double amount)
        {
            ...
        }
        ...
    }

then placing the following annotation on the Context class:

@ExtensionMethod(MoneyTransferContext.Account_Source.class)
public class MoneyTransferContext
{
    ...
    static class Account_Source ...


the result will be that we can do the following:

    public void executeSourceToDestinationTransfer(
        final Double amountToTransfer,
        final Account source,
        final Account destination)
    {
        source.transfer(destination, amountToTransfer);
    }

So, the "source" object gains the extension method "transfer" that does not exist in the Account class.
Just to be precise, the annotation @ExtensionMethod specifies *where* a certain extension method will be used (they probably should have named it UsesExtensionMethod ...)

According to the Lombok docs: "Calls are rewritten to a call to the extension method", so there's no object wrapping - the identity is preserved.

What's nice about the way the Lombok folks implemented extension methods is that they do not apply globally, but only where they are specified (think of it as role-binding inside the Context only).

The complete example can be found here: https://github.com/alexbalmus/dci_java_playground/tree/ext_method_lombok_approach

Please note that I've also added two new marker annotations @DciContext and @DciRole just to better express intentionality.

***

Alternatively, I've also searched for ways to implement DCI in pure Java (w/o Lombok), and the best I could come up with is something similar to the approach taken by Andreas Söderlund for TypeScript and described in his tutorial series: https://blog.encodeart.dev/series/dci-typescript-tutorial

Andreas implemented DCI in TypeScript using nested (inner) functions and a naming convention. Something similar can be achieved in Java using a combination of a generic functional interface (i.e. interface containing a single abstract method) and a Lambda expression to basically achieve something that's almost like a reference to an anonymous inner function.

So, if we have a functional interface such as:

    public interface RoleMethod<T>
    {
        void call(T t);
    }

Then we can have something similar to JavaScript nested functions like this:

    public void executeSourceToDestinationTransfer(
        final Double amountToTransfer,
        final A source,
        final A destination)
    {
        //----- Role methods:

        // Destination account:
        RoleMethod<Double> destination_receive = (amount) ->
        {
            destination.increaseBalanceBy(amount);
        };

        // Source account:
        RoleMethod<Double> source_transferToDestination = (amount) ->
        {
            if (source.getBalance() < amount)
            {
                throw new BalanceException(INSUFFICIENT_FUNDS); // Rollback.
            }
            source.decreaseBalanceBy(amount);

            // equivalent of: destination.receive(amount):
            destination_receive.call(amount);
        };


        //----- Interaction:

        // equivalent of: source.transferToDestination(amount)
        source_transferToDestination.call(amountToTransfer);
    }

Notice the naming conventions and calling of the (almost) inner functions:

source_transferToDestination.call(amountToTransfer);

and

destination_receive.call(amount);

Alas, the Java syntax does not allow to just simply write source_transferToDestination(amountToTransfer); instead you need to include the .call(...).

This is as good as it gets, but if inner functions are good enough an option for DCI in TypeScript, I suppose this approach should also be viable for Java.

Full example here: https://github.com/alexbalmus/dci_java_playground/tree/method_reference_approach

***

Let me know what you think.

Cheers!

Alex Balmuș

Lund Soltoft

unread,
Oct 2, 2025, 5:03:16 AM (11 days ago) Oct 2
to object-co...@googlegroups.com
In what scope are the extension methods available? I’m assuming that they are similar in nature to C# extension methods and have the same issue with scope that is they aren’t contained to the context. 

If my assumption is correct would also mean that the context itself is likely not going to be an object that could itself play a role in another context. 

That being said. The fundamental concept needed to do a preprocessor that rewrite the AST to allow for extension methods is essentially what I’ve done I my work since I created Marvin which was a fork of C# and I used the extension method rewriting but scoped it to the context. 



Mvh
Rune

Den 2. okt. 2025 kl. 10.30 skrev Alexandru Balmus <alexb...@gmail.com>:

Hi everyone,
--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/object-composition/7a7dbf2d-ffd1-4216-86c8-1e8e526aa6ddn%40googlegroups.com.

Alexandru Balmus

unread,
Oct 2, 2025, 5:34:02 AM (10 days ago) Oct 2
to object-composition
The scope is limited to the class on top of which the annotation  @ExtensionMethod is placed. As mentioned this annotation (perhaps a bit miss-named by the Lombok folks) is not used to define an extension method (an extension method candidate is any public static method containing at lest one param of the type to be extended) but rather it is used to specify that within the annotated class we can use the specified extension method. So if the annotation is placed on the Context object, the extension method will only be available there.


As can be seen, the  MoneyTransferContext's executeSourceToDestinationTransfer method wants to use source.transfer(...), therefore on top of  MoneyTransferContext I've placed the annotation @ExtensionMethod(MoneyTransferContext.Account_Source.class) to specify the class where this extension method was defined. Without this annotation this extension method would not be available.

Likewise, the .receive(...) extension method is only visible to the role class Account_Source who's public static transfer(...) method (the extension method used in the Context class) makes use of .receive(). Therefore this class is annotated with @ExtensionMethod(MoneyTransferContext.Account_Destination.class) to specify the class where the extension method is defined.
 
So extension methods are pretty well confined to the required scope.

Cheers,

Alex Balmus

Lund Soltoft

unread,
Oct 2, 2025, 8:30:39 AM (10 days ago) Oct 2
to object-co...@googlegroups.com
If I understand you correctly then any class with that annotation has access to the method?


Den 2. okt. 2025 kl. 11.34 skrev Alexandru Balmus <alexb...@gmail.com>:

The scope is limited to the class on top of which the annotation  @ExtensionMethod is placed. As mentioned this annotation (perhaps a bit miss-named by the Lombok folks) is not used to define an extension method (an extension method candidate is any public static method containing at lest one param of the type to be extended) but rather it is used to specify that within the annotated class we can use the specified extension method. So if the annotation is placed on the Context object, the extension method will only be available there.

Alexandru Balmus

unread,
Oct 2, 2025, 8:54:13 AM (10 days ago) Oct 2
to object-composition
I may have been a bit ambiguous about what I said earlier, so:
- any public static method who takes at least one param of an object type - in our case Account - are *candidates* to be extension methods for that object type; however, not all objects of type Account will have access to that new extension method, but only those objects of type Account that are used within a method of a class - in our case MoneyTransferContext - that has that annotation, specifying the extension method to make available. 

The Lombok page describes the use of the annotation, showing an example of using it vs the equivalent code without it: https://projectlombok.org/features/experimental/ExtensionMethod 

Lund Soltoft

unread,
Oct 2, 2025, 11:01:03 AM (10 days ago) Oct 2
to object-co...@googlegroups.com
Which makes the method publicly available and not scoped to the context. 

Which would lead to potential conflicts. Eg transfer might be used both when transferring money between accounts but also when transferring ownership _of_ accounts within each context however the meaning of transfer would be very exact. 

Mvh
Rune

Den 2. okt. 2025 kl. 14.54 skrev Alexandru Balmus <alexb...@gmail.com>:

I may have been a bit ambiguous about what I said earlier, so:

Matthew Browne

unread,
Oct 2, 2025, 1:50:50 PM (10 days ago) Oct 2
to object-co...@googlegroups.com, Lund Soltoft

Seems like that might be an acceptable limitation though? All the DCI implementations we have in existing languages are imperfect in some way. In the grand scheme of things, this doesn't seem like a deal-breaker, but I've only skimmed this so I could be missing something.

Matt Browne (he/him)

Alexandru Balmus

unread,
Oct 3, 2025, 7:11:16 AM (9 days ago) Oct 3
to object-composition
Not quite: for my example, although the  Account_Source::transfer(...) method is public static, it's visibility outside it's class will actually be constrained by the access modifier of its class.

To be precise:  Account_Source (where the extension method is defined) is a nested class inside MoneyTransferContext, and in my example has the implicit access modifier which is "package level" (a.k.a namespace level). 
This means that it's transfer(...) method will be visible only to  MoneyTransferContext (the top level class that houses  Account_Source and also has the specific annotation that permits usage of the extension method inside it) and also visible to other classes from the same package. 
If no other class sits in the same package with MoneyTransferContext, then the extension method will only be visible inside  MoneyTransferContext.  So not publicly available. 

The reason I did not make Account_Source  *private* static was because then the Lombok annotation wouldn't have been able to access it:
@ExtensionMethod(MoneyTransferContext.Account_Source.class) // the  .Account_Source would not have worked if Account_Source were private

So if we do not place any other class in the same package with  MoneyTransferContext then there will be no concern.
Reply all
Reply to author
Forward
0 new messages