Oskar Dudycz

Pragmatic about programming

Announcing Strictland - new contract testing library for message compatibility

2026-06-15 oskar dudyczTesting

cover

I just released something. It’s called Strictland. And it’s a contract testing library. Why did I do it?

Before I go further, if you can’t wait, you can check it on:

So now you already know that it’s a JVM (Java, Scala, Kotlin, etc.) Open Source library.

Let me first show you a sneak peek. Stop for a moment and have a look on this:

@Test
void ensureOrderPlacedCompatibilityWithNewerVersion() {
    // Strictland specification
    MessageContract.specification(Json.Jackson.of(yourObjectMapper))
        .given(new OrderPlaced(orderId, "Alice"))
        .whenDeserializedAs(OrderPlacedWithCoupon.class)
        .thenBackwardCompatible();
}

What would you say if you saw such a test? Think about it, we’ll get back to it.

What’s Strictland?

But again, why did I do it if there are mature solutions like Pact, Spring Cloud Contracts, or Confluent Schema Registry?

If you’ve used consumer-driven contract testing, the usual approach is to run both the provider and the consumer, record the consumer’s expectations against a mock, verify the provider against those expectations, and share those contracts through a broker.

In Strictland, I took a smaller, simpler approach. It serialises the message in a standard unit test and saves the output as a snapshot file that you commit alongside your code.

The test fails when the serialised shape changes, or when it contains a breaking change - up to you to specify expectations. A check confirms that an older and a newer version of the message can still read each other’s data (or the other way round).

Because it’s only serialisation and a file, the setup stays small:

  • The checks are ordinary unit tests in your existing suite, so there’s no broker, schema registry, or mock service to run, and nothing to start in Docker.
  • The contract is the serialised JSON committed alongside the test, so any format change appears in a normal diff and is reviewed like any other code.
  • You write the check beside the message it covers and get the answer in the same fast feedback loop as the rest of your tests. The check uses your application’s own serializer, so the snapshot is the exact bytes you ship.
  • Strictland checks the serialised shape of a message and whether its versions stay compatible. It doesn’t exercise a live exchange between running services, so it complements that kind of tooling rather than replacing it.

Strictland checks the serialised shape of a message and whether its versions stay compatible. It doesn’t exercise a live exchange between running services, so it complements that kind of tooling rather than replacing it.

It’s not as powerful as popular tooling, but it’s also much simpler to start catching our mistakes. Traditional solutions allow you to mock protocols, put a man in the middle, and even generate client code. All of that is great if you need it and have experience with it.

Most of the customers I’m helping through consultancy and training aren’t there yet. Setting up those tools is a lot of heavy lifting for them and adds additional complexity. And well, maybe they don’t need to be, as this approach served me well in my past projects. I always handcrafted such a tool in my projects, but finally decided to make it properly.

Why such a name?

I’m putting (too?) much effort into my projects’ names. It’s a word game. Contract testing rewards a strict approach to your message shapes, and Mr. Strickland was strict enforcer in Back to the Future. That puts it in good company next to its sibling Emmett, named after Doc Emmett Brown. I also didn’t want to collide with the older JS validation package.

How to use it?

Getting back to the essence… How to use it?

You write a small unit test that locks down a message’s format. Later, you rename a field, change a type, or adjust how a value serialises; the code still compiles and your other tests pass, but that one fails and points at what changed. You fix it in your build before a consumer or a stored event has hit the old format in production.

When a message changes by accident, a snapshot check shows you exactly what moved. When you evolve a message on purpose, a compatibility check confirms an old and a new version can still read each other’s data.

You can start by installing it from Maven Central, by adding dependency.

<dependency>
  <groupId>io.event-driven</groupId>
  <artifactId>strictland</artifactId>
  <version>0.3.0</version>
  <scope>test</scope>
</dependency>

Then adding such test:

MessageContract.specification(Json.Jackson.defaults())
    .given(new OrderPlaced(orderId, "Alice", placedAt))
    .whenSerialized()
    .thenContractIsUnchanged();

The first run serializes the message and writes the result to an approved file named after the class, OrderPlaced.approved.txt, saved next to the test:

{"orderId":"00000000-0000-0000-0000-000000000001","customer":"Alice","placedAt":"2024-01-01T12:00:00Z"}

You review that file and commit it. From then on the check compares against it and fails if the format drifts, so a later change to the format shows up in the same pull request as the code that caused it.

A message under contract goes through one of two checks.

A snapshot check confirms the message still serializes exactly as it did when you last approved it, so nothing reading it downstream breaks. A failure means the format changed: a field renamed, a date format switched, a value newly dropped or added.

A compatibility check is for the version you evolve on purpose, so changing a message doesn’t leave the ones already in your store or on the wire stranded. Use thenBackwardCompatible() to confirm the newer version still reads a message the older one wrote, the events you stored last year, or a request already sent. Use thenForwardCompatible() to confirm a reader that hasn’t upgraded yet still reads a message the newer version writes, so you can ship the new shape before everyone reading it has caught up. Both compare the fields the two versions share and fail if a required one is missing or a shared value has changed.

@Test
void ensureOrderPlacedCompatibilityWithNewerVersion() {
    // Strictland specification
    MessageContract.specification(Json.Jackson.of(yourObjectMapper))
        .given(new OrderPlaced(orderId, “Alice”))
        .whenDeserializedAs(OrderPlacedWithCoupon.class)
        .thenBackwardCompatible();
}

You review that file and commit it. From then on the check compares against it and fails if the format drifts, so a later change to the format shows up in the same pull request as the code that caused it.

As you see, in the specification, you pass your serializer.

var spec = MessageContract.specification(Json.Jackson.of(yourObjectMapper))

I encourage you to use your application’s object mapper and pass the same ObjectMapper it uses, so the test checks the exact bytes you ship. That’ll help you avoid nasty surprises, where tests are using a different format than your framework or tooling.

Strictland provides an implementation of a sensible Jackson setup: ISO-8601 dates, nulls kept, unknown properties ignored on read. You can use it with Json.Jackson.defaults(). But it’s more of an accessible thing to get you started quickly.

You can also define your own serializer if you’re using an unsupported (yet?) format or serializer type. See basic examples in:

Should you use it?

Yes, you do!

Strictland is young and pre-1.0, so the API can still move between versions. The checks themselves are small and well covered, and the snapshots they produce are just files in your repository, so trying it out costs little and commits you to nothing.

I’d genuinely like your feedback. If something is missing or awkward, tell me through comments, on Discord or open an issue.

Currently, I’m working on making snapshot location more organised and configurable, not to end up with a bloated snapshots tests folder.

I also need to rethink the given name; it’s from Behaviour-Driven Design, but it appears to be a reserved word in Scala 3. Cross-language and cross-stack libs are much fun!

For now, for JVM, as the Java-based customers I’m helping also needed that, but soon want to add .NET and TypeScript/Javascript (and maybe more).

Tell me your honest thoughts, give it a try and a star if you liked it. Play with it, and pass me your feedback. It’ll motivate me to continue working on it!

Would you like to learn more about consumer-driven tests and how Strictland can be used for them?

And read in the event versioning series:

Cheers!

Oskar

p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.

👋 If you found this article helpful and want to get notification about the next one, subscribe to Architecture Weekly.

✉️ Join over 11500 subscribers, get the best resources to boost your skills, and stay updated with Software Architecture trends!

Loading...
Event-Driven by Oskar Dudycz

cover

Through my window, I see the result of good plans but poor execution. Opposite my flat, there is a partially completed construction place. Buildings were supposed to be eye-catching Mediterranean style apartments. Delivery date? Two years ago. Actual? More and more unknown.

Some time ago, I heard that using Event Sourcing makes creating Event-Driven Architecture easier. The arguments were correct, that if we’re already publishing events to trigger business workflows, then at some point, we may want to also store events to not lose information. Agreed. However, I also heard that keeping the state as events will simplify things. We’ll have a source of truth with a record of the system behaviour. This will allow, e.g. to confront the results of the operations with the recorded state. I’d agree with that, with one distinction. It’s easier as long as you already know Event Sourcing.

Many people in the DDD community claim that the essential is to properly break down the system into autonomous parts called bounded contexts. Once we have it, the rest is secondary and will sort itself out. For sure.

Many seasoned programmers speak similarly about new technologies. They claim that they can translate past experience into new technologies. That’s true that by analogy, they can catch the big picture quicker. But isn’t it a bold assumption to say that Win.Forms specialist will learn Angular quickly?

The end result may differ a lot from the initial ideas. I saw the plan of those buildings next to me. Now I can see the effects of the execution. Or actually, the lack.

I believe that we should carefully acknowledge not only the point of view of our authorities but also their seating point. If we want to find out how to form a wall, do we ask an architect or a foreman? An architect may know the theory, but the practice is what we’re looking for. On the other hand, if you want to know where to put the wall, you prefer the architect to do measurements. At least if you don’t want to have the roof falling to your head.

After I had torn a ligament in my knee, I went to two qualified orthopedists. One said I should have surgery and do a reconstruction. The second stated that there is no need for that; rehabilitation should be enough. Guess which one had a specialization in surgery and which in rehabilitation?

People usually give us advice from the point where they’re currently standing. They are entitled to a biased view. An architect who rarely does programming will tend to downplay the value of implementation and tactical patterns. Midlevel developers will focus on technicalities instead of the global system impact. The team manager or consultant will emphasize the importance of soft skills (or esoteric techniques known only to them).

The truth is that we need all of them. The excellent plan will fall on the bad execution. The best execution for the wrong case will be just a waste of time. We should carefully evaluate the advice considering what we need and what an expert can give us.

Therefore, when we’re reading an article, watching a talk, let’s also pay attention to the place where the person is standing. The perspective from there may be much different from where we are right now. That can be good, as it may push us in the right direction. But it may also be misleading, as we accidentally take biases of this person without understanding the tradeoffs. Personally, I prefer to follow not only people from pedestal but also those that are closer to my position. A bit further in the journey, but not too far. That helps me to calibrate my view as those people are more relative to my daily struggles.

Polish historical leader Józef Piłsudzki reportedly used to say: “Right is like an ass, everyone has its own”.

Cheers!

Oskar