Published on

The ambiguity of implementation details: mocking, interactions and abstractions

Authors

"Just send e-mail"

Imagine that two developers - Daniel and Alice - are working on a new feature for their application.

It is related to sending e-mails, whenever an order was placed.

Sounds quite typical, right?

It does not need to be explained, as it's a common feature in many applications.

Of course, they are following 3S workflow (also known as TDD), so they use consumer-expected needs to drive the implementation.

Fortunately for them, an executable specification was there already:

[Fact]
public async Task given_an_order_when_placing_order_then_order_is_saved()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing = 
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    await _client.PlaceOrder("/orders", orderToPlace);

    await orders.Received(1).Save(Arg.Is<Order>(o => o.Id == "order-123"));
}

They needed to grow the system by introducing the new, required behavior.

But they didn't want to express all consumer requirements in a single test, so that from designer's perspective they could more easily locate the problems using very precise specifications:

[Fact]
public async Task given_an_order_when_placing_order_then_confirmation_email_is_sent()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing = 
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .WithCustomerEmail("a@b.com")
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    await _client.PlaceOrder("/orders", orderToPlace);

    var emailSender = _factory.Services.GetRequiredService<IEmailSender>();

    await emailSender.Received(1).Send("a@b.com", "Your order has been placed."); 
}

Daniel wasn't happy that compiler was buzzing, as red color is associated with something "bad".

However, Alice calmed him down by saying that they just expressed (or specified) needs, which now requires satisfying.

Orders interface was already there, so they could focus on implementing the missing, new behavior.

To make everything compiling, Daniel firstly created missing interface:

public interface IEmailSender
{
    Task Send(string recipient, string message);
}

Right after that, they went to the easiest implementation, that satisfies the requirements.

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    Orders orders,
    IEmailSender emailSender
) =>
{
    var order = orders.Get(request.OrderId);
    order.Place();
    await orders.Save(order);
    await emailSender.Send(order.CustomerEmail, "Your order has been placed.");
    return Results.Ok();
});

record PlaceOrderRequest(string OrderId);

public interface Orders // ✅ it was already here, so no need to create it again
{
    Order Get(string id);
    Task Save(Order order);
}

They ran both tests and got green lights.

But Daniel wasn't happy - he heard that using mocks to verify if methods on objects were invoked is a bad practice.

He suggested going with WireMock and testcontainers in order to not worry about "internals", also known as "implementation details".

Alice asked him a bunch of questions in order to understand his mental model, when it comes to designing.

It quickly turned out that he associated "mocking" with "implementation details", because that is what major of the software engineering community is saying so.

She tried to unfold another way of thinking that was more oriented toward business capabilities - if there is a need for saving information about placed order, that is a hard requirement that cannot be omitted, rather than thinking in technical terms.

So ensuring that the e-mail was correctly sent and that the placed order was saved became a matter of fulfilling business needs, not just verifying technical interactions.

"Sometimes e-mail, sometimes SMS"

Change, change never changes.

It quickly turned out that this was a banger feature - finally customers were able to be informed right after their order was placed.

But not everyone wanted to use e-mails for achieving so.

Some customers wanted to receive notifications via SMS instead.

"Notifications", Alice said aloud, starting to realize their designing mistake.

Daniel didn't get it - "what mistake was she talking about?" - he wondered.

She went to the specs and understood that they did not discover true abstractions, but rather technical mechanisms for deliverying notifications.

"We need to refactor", she said to her fellow pair-designer.

"What? Oh come on, this feature is almost there, gimme the keyboard", Daniel protested.

Thankfully, he worked with Alice so he had the habit so start with a specification (or rather adjusting it):

[Fact]
public async Task given_preference_set_to_email_and_an_order_when_placing_order_then_confirmation_email_is_sent()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .WithCustomerEmail("a@b.com")
        .WithNotificationPreference(NotificationPreference.Email)
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    await _client.PlaceOrder("/orders", orderToPlace);

    var emailSender = _factory.Services.GetRequiredService<IEmailSender>();
    await emailSender.Received(1).Send("a@b.com", "Your order has been placed.");
}

This test failed when executed, so Daniel provided the implementation:

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    Orders orders,
    IEmailSender emailSender
) =>
{
    var order = orders.Get(request.OrderId);
    order.Place();
    await orders.Save(order);

    if (order.NotificationPreference == NotificationPreference.Email)
        await emailSender.Send(order.CustomerEmail, "Your order has been placed.");

    return Results.Ok();
});

And poof, specs were green again.

"Now, time for another channel!", shouted Daniel.

[Fact]
public async Task given_preference_set_to_sms_and_an_order_when_placing_order_then_confirmation_sms_is_sent()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .WithCustomerPhone("+48123456789")
        .WithNotificationPreference(NotificationPreference.Sms)
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    await _client.PlaceOrder("/orders", orderToPlace);

    var smsSender = _factory.Services.GetRequiredService<ISmsSender>();
    await smsSender.Received(1).Send("+48123456789", "Your order has been placed.");
}

Satisfying new requirements happened just after that spec:

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    Orders orders,
    IEmailSender emailSender,
    ISmsSender smsSender
) =>
{
    var order = orders.Get(request.OrderId);
    order.Place();
    await orders.Save(order);

    if (order.NotificationPreference == NotificationPreference.Email)
        await emailSender.Send(order.CustomerEmail, "Your order has been placed.");

    if (order.NotificationPreference == NotificationPreference.Sms)
        await smsSender.Send(order.CustomerPhone, "Your order has been placed.");

    return Results.Ok();
});

And tests were green again.

How good feeling is this, ain't it dear Reader?

But Alice remained silent throughout this entire 3S cycle.

"We need to simplify it, it was very easy to add this implementation, but any time we do something with notifications, we would need to touch application logic.", she concluded.

She also pointed out that number of dependencies increased, which is a strong signal that they are probably missing an abstraction.

Her pair-designing partner didn't get all of these - tests were green, implementation was easy - why worry about abstractions?

Alice asked for a permission to add an alternative implementation, just to see the options they had.

She asked herself aloud what are the current needs and it turned out they were discussing notification preferences.

That was their WHAT, and somehow they already framed themselves in using Order as the primary source of preferences.

But order placement capability never specified HOW it needs to be provided.

So she started refactoring by introducing missing concepts and structures:

[Fact]
public async Task given_preference_set_to_email_and_an_order_when_placing_order_then_confirmation_email_is_sent()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .WithCustomerEmail("a@b.com")
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    var notificationPreferenceProvider = 
        _factory
        .Services
        .GetRequiredService<IProvideNotificationPreference>(); // ❌ bzzzzzt, no such interface!
    
    notificationPreferenceProvider
        .For(orderAwaitingPlacing)
        .Returns(NotificationPreference.Email);
    var emailSender = _factory.Services.GetRequiredService<IEmailSender>();

    await _client.PlaceOrder("/orders", orderToPlace);

    await emailSender.Received(1).Send("a@b.com", "Your order has been placed.");
}

Then she created missing interface:

public interface IProvideNotificationPreference
{
    NotificationPreference For(Order order);
}

Followed by adjusting the implementation:

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    Orders orders,
    IProvideNotificationPreference notificationPreferenceProvider,
    IEmailSender emailSender,
    ISmsSender smsSender
) =>
{
    var order = orders.Get(request.OrderId);
    order.Place();
    await orders.Save(order);

    var preference = notificationPreferenceProvider.For(order);

    if (preference == NotificationPreference.Email)
        await emailSender.Send(order.CustomerEmail, "Your order has been placed.");

    if (preference == NotificationPreference.Sms)
        await smsSender.Send(order.CustomerPhone, "Your order has been placed.");

    return Results.Ok();
});

She also refactored other specs, accommodating the change of introducing a new interface.

By specifying the interface and interaction in advance, by using NSubstitute (a tool for designing using mocking techniques), she designed the usage of that capability and the contract.

Daniel was upset - jira ticket did not say anything about providing notification preferences.

It was so natural to associate preferences with orders - and it required so little changes.

Alice challenged that perspective by asking what if preferences coming from order is true for today, but tomorrow there will be a dedicated web service providing those - what then?

"We will change implementation, simple.", Daniel replied.

But Alice was not finished, she started pulling more capabilities out of the existing implementation:

[Fact]
public async Task given_preference_set_to_email_and_an_order_when_placing_order_then_customer_is_notified()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    var notificationPreferenceProvider = 
        _factory
        .Services
        .GetRequiredService<IProvideNotificationPreference>();

    notificationPreferenceProvider
        .For(orderAwaitingPlacing)
        .Returns(NotificationPreference.Email);

    await _client.PlaceOrder("/orders", orderToPlace);

    var notification = _factory.Services.GetRequiredService<ISendNotification>(); // ❌ bzzzzzt, no such concept expressed using code!
    await notification.Received(1).Send(NotificationPreference.Email, orderAwaitingPlacing, "Your order has been placed.");
}

Daniel silently observed what the heck she is doing - "new interfaces?!" - he thought.

Alice added missing interface:

public interface ISendNotification
{
    Task Send(NotificationPreference preference, Order order, string message);
}

And adjusted other tests, so that they become green again.

But Daniel couldn't understand - why so many new interfaces?

Now they are not 100% sure if the entire system is working, because they are testing against fake implementations!

In his opinion, it will be easier to use the real implementations and test the system end-to-end, rather then testing implementation details.

Alice to finalize her approach, she showed how real implementations might look like:

public class OrderBasedNotificationPreference : IProvideNotificationPreference
{
    public NotificationPreference For(Order order) => order.NotificationPreference;
}

public class InProcessNotificationGateway : ISendNotification
{
    private readonly IEmailSender _emailSender;
    private readonly ISmsSender _smsSender;

    public InProcessNotificationGateway(IEmailSender emailSender, ISmsSender smsSender)
    {
        _emailSender = emailSender;
        _smsSender = smsSender;
    }

    public async Task Send(NotificationPreference preference, Order order, string message)
    {
        if (preference == NotificationPreference.Email)
            await _emailSender.Send(order.CustomerEmail, message);

        if (preference == NotificationPreference.Sms)
            await _smsSender.Send(order.CustomerPhone, message);
    }
}

"Maybe you were correct, after all? Placing an order does not care about the implementation details of notification sending", she concluded again.

Daniel nodded in agreement.

He knew that he was right all the time.

He awaited Alice's next refactoring, removing that artificially created interface.

So he observed in silence.

[Fact]
public async Task given_an_order_when_placing_order_then_customer_is_notified()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    await _client.PlaceOrder("/orders", orderToPlace);

    var notification = _factory.Services.GetRequiredService<ISendNotification>();
    await notification.Received(1).Send(orderAwaitingPlacing, "Your order has been placed.");
}

Alice smiled, because of the next specification:

[Fact]
public async Task given_email_preference_when_sending_notification_then_email_is_sent()
{
    var order = 
        Order
        .AnyAwaitingPlacing()
        .WithCustomerEmail("a@b.com")
        .Build();

    var preferenceProvider = Substitute.For<IProvideNotificationPreference>();
    var emailSender = Substitute.For<IEmailSender>();
    var gateway = new InProcessNotificationGateway(preferenceProvider, emailSender, Substitute.For<ISmsSender>());

    preferenceProvider
        .For(order)
        .Returns(NotificationPreference.Email);

    await gateway.Send(order, "Your order has been placed.");

    await emailSender.Received(1).Send("a@b.com", "Your order has been placed.");
}

She ensured that all scenarios were covered, making all tests green too.

But there was one thing that was really off - "why notification gateway does need to know anything about orders?", Alice wondered.

She thought of introducing another concept - a NotificationContent - but decided to leave it, for now.

They had a very vivid discussion about the whole development process - Alice tried to understand the standpoint of her pair-designer, highlighting the bright sides of using WireMock/testcontainers for the whole test suite.

Although, she disagreed with making specs much bigger, including many assertions.

Having the possibility to localize problems through well-defined expectations about interactions, using well-defined contracts, was a must-have in her world.

And then new requirements arrived.

"Notify about fact"

This time it was Jerry, the architect, who came with a requirement: there are other parties interested in a fact of placing an order.

The idea was simple: emit an event, stating that order was placed, and let others deal with that information.

"Of course, one of the consequences will be notifying customers about that fact using e-mail or SMS." Daniel assumed.

"Finally, we won't deal with e-mails", he assumed again.

"Actually, it's a bit opposite. I want to have two ways at the same time - I don't know what is the customer experience if we notify them eventually", said the architect.

Daniel couldn't believe - how the hell it makes any difference? For the end user it's a freaking notification, being delivered, but eventually, because there will be another service doing so.

"But it will happen, for sure", he said aloud.

Alice was more curious and asked about what is the decision point, when it comes to sending e-mail immediately rather than eventually.

"Together with product owner, we believe that some customers treat notifications very seriously and they want to get that information as soon as possible", replied Jerry.

"So we need to send e-mails either immediately or eventually, and always emit events, right?" Alice asked.

"Yes, but please remember that Notification service is serving multiple consumers and it can't accept specific information, like Daniel suggested, by using events", Jerry observed.

"And please remember we're experimenting to see what will be the customer response, when it comes those deferred notifications", he continued.

They know everything that was required to fullfil those requirements.

Daniel was so eager earlier to go fully with Event-Driven Architecture, but now he needs to await.

So Alice started thinking how to satisfy those requirements.

She wanted to express new concept - selecting Notification gateway - following that abstract idea in tests.

[Fact]
public async Task given_an_order_when_placing_order_then_customer_is_notified()
{
    var orderToPlace = new { OrderId = "order-123" };

    var orders = _factory.Services.GetRequiredService<Orders>();
    var orderAwaitingPlacing =
        Order
        .AnyAwaitingPlacing(withId: orderToPlace.OrderId)
        .WithCustomerId(customerId: "customer-ABC")
        .Build();

    orders
        .Get(orderToPlace.OrderId)
        .Returns(orderAwaitingPlacing);

    var notification = Substitute.For<ISendNotification>();
    var gatewaySelector = 
        _factory
        .Services
        .GetRequiredService<ISelectNotificationGateway>(); // ❌ bzzzzt, no abstract idea expressed in the code yet!

    gatewaySelector
        .SelectFor("customer-ABC")
        .Returns(Task.FromResult(notification));

    await _client.PlaceOrder("/orders", orderToPlace);

    await notification.Received(1).Send(orderAwaitingPlacing, "Your order has been placed.");
}

To calm down the compiler, new interface was immediately added:

public interface ISelectNotificationGateway
{
    Task<ISendNotification> SelectFor(string customerId);
}

Daniel was shocked - mock returning mock?

"This is such an anti-pattern", he commented aloud.

Alice just mentioned that she used it to design interactions and contracts.

"Internal ones, right?", he continued.

"Well, yes, capabilities required by order placement, according to our new understanding", she replied.

Alice satisfied specification with the following implementation:

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    Orders orders,
    ISelectNotificationGateway gatewaySelection,
    IPublishEvent publisher
) =>
{
    var order = orders.Get(request.OrderId);
    order.Place();
    await orders.Save(order);

    await publisher.Publish(new OrderPlaced(order.Id));

    var gateway = await gatewaySelection.SelectFor(order.CustomerId);
    await gateway.Send(order, "Your order has been placed.");

    return Results.Ok();
});

This sparked a new conversation, related to ratio of customers that should use deferred notifications. It turned out that it should be no more than 5% of customers.

According to the established understanding, the follwing implementation happened:

public class ExperimentalRandomizedNotificationGatewaySelection(
    InProcessNotificationGateway immediateNotificationGateway,
    NotificationWebGatewayHttpClient eventualNotificationGateway
) : ISelectNotificationGateway
{
    public Task<ISendNotification> SelectFor(string customerId)
    {
        ISendNotification gateway = Random.Shared.NextDouble() < 0.05
            ? eventualNotificationGateway
            : immediateNotificationGateway;

        return Task.FromResult(gateway);
    }
}

They shipped it, awaited some time period and observed the results.

It turned out that some customers were happy to pay a little more in order to get notification in a guaranteed time.

Daniel was surprised by the unexpected outcome.

He couldn't believe that customers were willing to pay more for notifications.

Alice explained that it was not about notifications, but about purchasing priority.

It might have sounded as it was a mere detail, but that framing was crucial for understanding customer behavior.

And stop "just coding" and start thinking in terms of producing value, as customers would have never think of that themselves.

As this feature was proven to be valuable, they decided to provide it only for subscribed customers, which drove the implementation toward the following capability:

public class SubscriptionBasedNotificationGatewaySelection(
    ICheckCustomerSubscription subscriptions,
    InProcessNotificationGateway immediateNotificationGateway,
    NotificationWebGatewayHttpClient eventualNotificationGateway) : ISelectNotificationGateway
{
    public async Task<ISendNotification> SelectFor(string customerId) =>
        await subscriptions.HasSubscribedToNotificationPriority(customerId)
            ? immediateNotificationGateway
            : eventualNotificationGateway;
}

Fortunately, there was company-wide subscriptions web service that provided such capabilities, so it was very easy to integrate.

And thankfully, the structure, that emerged through gaining understanding, was ready for accommodating incoming changes.

Implementation details?

"Come on, mocking in 2026 with AI?!", some might think right now.

Well, yes.

It is not mocking that is "bad" on its own, it's the operator that truly makes the difference.

As we discussed some time ago - everything boils down to composing a PIE.

Based on Purpose (driven by needs) we design Interactions between Elements.

We saw two developers building understanding of what needs to be delivered and then they designed proper communication structure between elements.

Alice mindfully organized this communication letting certain abstractions to emerge.

Not NotificationService or NotificationManager, but symbols and ideas that were appearing in human language, as they spoke and listened to each other.

NotificationContent, NotificationPreference, NotificationGateway and so on.

These abstractions allowed them to reason about the system at a higher level, focusing on the value delivered to the customer rather than the mechanics of implementation.

Considering they properly abstracted away details.

But really, what are implementation details?

Often time one might heard that mocks are "bad" because they make the tests brittle and tightly coupled to the implementation.

Same as the claim that mocks are breaking encapsulation - what is really encapsulation? (you might be interested in a great article by Grzegorz Gałęzowski about alleged encapsulation breaking and why we might incorrectly think about it)

As yet again we get trapped in thinking in static labels, names - "mocks".

What if mocking is actually a design tool?

So it is a process of designing. Designing what?

Interactions.

How certain concepts interact with each other.

What are relationships between abstractions, but not in a static, "data" sense, but when those abstractions are in motion, when they "act".

To understand how they collaborate behaviorally.

Or rather, how do we want them to collaborate with each other.

It might be that those "implementation details" are actually interaction requirements we specify (as designers) so that certain abstractions can provide them.

Abstractions we introduce so that we can raise the level of being specific.

Abstractions that aim at creating a specific boundary that expresses certain language.

Often times those "implementation details" might create an effect that some call "brittle tests from over-mocking" - but if actually those "brittle tests" are diagnostic signal trying to highlight poor abstraction discipline?

Expressing roles and their interactions that might be actually quite stable, if we model them at certain level.

It might quickly turned out that interfaces do not represent the correct behavioral boundaries, but are rather a reflection of our current understanding.

Or rather - misunderstanding.

And shallow analysis of the problem area.

Poor abstraction discipline is frequently a symptom of implementation-first thinking, leaking details that should have been hidden inside a particular boundary.

And it is our job to understand the problem, language that lives inside of that area, decompose the purpose into a set of collaborating roles, named after what they do, not what they are or how they do it.

Translating that purpose into interactions of elements, provided that we build proper structure around our understanding, will bring stable abstractions, without the dreaded details.

Maybe those details are poorly designed abstractions, dear Reader?

"Consumer observable behavior" you fool!

What about notifying customers using some kind of preferred delivery mechanism?

Does it really matter how those notifications are delivered?

Isn't it "the implementation detail"?

Well, some might say yes, some might say no.

Then, why "no"?

Behavior is exactly the same, isn't it?

Well, what if an external Notification web service has a priority queue and some customers might be notified later than others?

Behavior might be exactly the same, but experience will significantly vary.

That is why quality attributes are truly important - they add more dimensions to the provided solution.

Those quality attributes emerge from the interactions between abstractions, that form boundaries, hiding certain details, from consumers' sight.

Features is the easy part.

Expressing important abstractions and how they interact - "the structure" - this is often the hard part.

It might turn out quickly that the notification delivery mechanism might actually matter - as it changes the reception of our provided capabilities.

One might conclude that there is a crucial nuance that the consumer is relative to the boundary you're looking at - that's why Outside-In Designing is so powerful, because one is traversing various boundaries and then designing accordingly.

"Implementation detail" is not an absolute property of a piece of code. It is a relational concept — defined by who is observing and from which boundary (or from what level).

And if mocking (a workflow/a process) is a designing technique that might teach us about interactions and boundaries - how does it contribute to thinking on other levels, outside of a single boundary?

EDA - Eventing Dopamine Addiction

Let's try to visualize the simple workflow using Event Storming notation.

Orange stickies are events, blue stickies are commands and yellow stickies are rules.

Exploring the problem

To better understand the needs, we could follow up with a question - "what is a consequence of placing an order?"

And some might of course come up with a notification - e-mail one.

Important information revealed

And as Daniel immediately saw - event here, event there, so placing order abstraction interacts with notifications in a very particular way - notifications consume events to trigger their behavior.

But is it the only way boundaries can interact?

It might be that designed interaction is not the correct one and might be quite costly if we continue down this path.

Another way of organizing interactions between boundaries - green stickies are information, pink stickies are external systems

That is what Jerry tried to communicate - some abstractions are much more stable than others and interactions should be designed accordingly.

If one organizes interactions incorrectly then details from one boundary might leak into another, causing unintended consequences and increasing complexity, operational costs and simply frustration.

Notifications are pretty stable concept (abstraction), whereas Orders might change more frequently - sometimes because of the business side, other times because of regulation.

Also, notice the language change - notifications are not using language of orders.

Inside their boundary there is nothing like "order" - there are recipients, notification contents, notification preferences, maybe notification priority?

As with "brittle tests from over-mocking" (which is a misnomer), coupling to details might be a wrong way of organizing interactions between elements - that are simply abstractions, but sometimes expressed on various levels.

Subsystems (Notifications) or small objects (Notifications).

Essentially, poorly designed abstractions (which means - behavioral models) is what we typically might be dealing with, but "coupling to implementation details" is a consequence of that.

Focus on finding proper abstractions rather than fixating on implementation details.

They are truly a detail (pun intended) when it comes to a process of organizing interactions between boundaries.

Mocking? DDD? Abstractions?

In Domain-Driven Design we say and think a lot about models expressing contextual behaviors - which can be translated into abstractions interacting with each other.

Relating to each other, in certain moments of time, in specific way.

If one uses brittle tests and challenging integrations between bounded contexts as diagnostic signals, we might eventually learn and improve ways how elements interact with each other.

Elements having different meaning, depending on the level we are currently reasoning at.

Which might lead to finding proper models or abstractions, or boundaries, or whatever.

It was always about the same thing, although the names might be changed.

Every brittle test is a friend in disguise - use it to guide you towards better abstractions and interactions.

Every challenging integration is an opportunity to refine boundaries, improve the language and communication structure.

Next time dear Reader when your tests are failing or you need to participate in yet another meeting about integration between services, ask yourself

Question 🤔

What those signals are trying to tell me?

Is there a better way to organize interactions or abstractions?

Did I represent abstractions correctly?

What abstract concepts am I missing?