Spring Boot · Apache Wicket · Spring Modulith · MongoDB · AI agents
Ship whole features with a team of three.
Most business software doesn't need a frontend team, a backend team and an API between them.
With plain Java from the database to the button, two or three people can own every feature end to end,
and AI agents take the routine work off their hands.
fullstack4j is how allwerker, a Munich cooperative of senior
engineers, sets up, staffs and coaches such teams.
One language, one deployableArchitecture checked on every buildOpen source, Apache 2.0Backed by 19 senior engineers at allwerker
One feature, one slice, one owner, from database to button.
The hidden cost
Your stack quietly decided your team size.
Choose a JavaScript single-page app, a REST API and a Java backend, and you have also chosen an org chart:
frontend developers, backend developers, two build pipelines, and an API contract to negotiate for every
feature. Each piece is reasonable on its own. Together they turn every user story into a relay race.
Split by technology
The usual setup
One feature becomes tickets in two or three backlogs, done by different people at different times.
Two languages, two build chains, two dependency treadmills to keep up with.
An API between your own UI and your own backend that no user ever asked for.
Work waits on whoever is the specialist for the layer that's next.
Split by feature
The fullstack4j setup
One feature is one slice, built and shipped by one person or pair.
Java and HTML templates. One build, one deployable, one set of dependencies.
The UI calls the service directly. Boundaries sit between features, where they matter.
Anyone on the team can pick up the next most valuable thing.
36
paths between 9 people
3
paths between 3 people
paths = n(n−1) / 2
Every line is a meeting, a handoff or a misunderstanding waiting to happen. Triple the team and you
get twelve times the coordination. Adding people to a split stack mostly buys you more lines.
Removing the layers that force the split is the cheapest way to make a team smaller and faster.
The approach
Small teams. Whole features. Plain Java.
Nothing here is new or exotic. It is mature, well-documented technology, arranged so that a small team
can understand the whole product and change any part of it safely.
01
Slices, not layers
Each feature lives in one module: its Wicket page, its use cases, its MongoDB documents and repository, and the
events it publishes. One person or pair takes it from idea to production. There are no
"frontend part" and "backend part" tickets.
02
One language, one deployable
Server-rendered UI with Apache Wicket: Java components and plain HTML templates, Ajax built in.
No JavaScript build chain, no client state to sync, no API contract with yourself. One Spring Boot
jar to test, ship and run.
03
Boundaries are tested, not documented
Spring Modulith turns each slice into a module that shows the others only its base package: its
page, its events and, where an event won't do, an interface that returns plain records. The
fullstack4j rules, built on ArchUnit, add the house rules: what a slice may show, where its
documents live, which slice sends which event. When code reaches
into another slice's internals, the build fails. Architecture stays intact while people, and agents, write
code quickly.
04
AI as the fourth team member
A uniform, typed, well-tested codebase with enforced boundaries is where coding agents do their best
work. People decide what to build and write it down in the slice itself; agents build until its
tests pass; people review the result.
Anatomy of a slice
What a feature looks like when one person owns it.
Everything a feature needs, in one package you can read in ten minutes. This is ordinary Wicket, Spring Boot
and Spring Data MongoDB. The only rule is where things go.
src/main/java/com/acme/shop
shop/
├── ShopApplication.java
├── StartPage.java links to each slice's page
├── package-info.java the map: slices, flow, words
├── ordering/one slice, one module
│ ├── package-info.java the spec: what it promises
│ ├── OrdersPage.java UI, plain Wicket
│ ├── OrderShipped.java event for other slices
│ └── internal/hidden from other slices
│ ├── Orders.java use cases
│ ├── Order.java one MongoDB document
│ └── OrderRepository.java MongoRepository
├── billing/listens to OrderShipped
└── shipping/src/main/resources/…/shop/
├── StartPage.html
└── ordering/
└── OrdersPage.html markup, no JS buildsrc/test/java/…/shop/
├── ArchitectureTests.javaModulith + fullstack4j rules
├── SpecTraceabilityTests.javaspec and tests in step
├── TestShopApplication.javadev run on MongoDB
└── ordering/
└── OrdersTests.javaone test per requirement
Readable by anyone, starting with the spec. No frontend folder and backend folder to keep in sync. The slice's package-info.java says what it promises in one screen, and it is the first thing a new colleague or an agent reads.
Small enough for one head, and for one agent's context window.
Guarded by tests.billing can react to an order being shipped because OrderShipped sits in ordering's base package; everything in internal stays hidden from it. And every requirement in the spec has a test that carries its id.
Events first, calls when needed. Slices react to each other's events wherever they can. When one really has to ask another something, it calls an interface in that slice's base package, which answers with records defined right next to it; the implementation and the documents stay in internal.
Stored as documents. An order and its lines are one MongoDB document, shaped like the code, so the schema grows with the slice instead of through migrations.
/// # Ordering
/// > Take a customer's order and see it through until it ships.
///
/// ## Operations
/// - `place-order`: take an order for a customer (`Orders.place`)
/// - `open-orders`: list what still has to ship (`Orders.open`)
/// - `ship-order`: hand an open order over to shipping (`Orders.ship`)
///
/// ## Requirements
/// ### R1: Place an order
/// - R1.1 — When a customer places an order with at least one line,
/// the slice shall list it as open.
/// - R1.2 — If an order has no lines, then the slice shall reject it.
///
/// ### R2: Ship an order
/// - R2.1 — When an open order is shipped, the slice shall no longer
/// list it as open.
/// - R2.2 — When an open order is shipped, the slice shall publish
/// `OrderShipped`.
/// - R2.3 — If the order has already shipped, then the slice shall
/// reject the request.
///
/// ## Events
/// - publishes `OrderShipped` (R2.2)
/// - listens to nothing
///
/// ## Entities
/// - `Order`: one document with its customer, lines and status
///
/// ## Out of scope
/// - prices and invoices (`billing`)
/// - parcels and carriers (`shipping`)
@ApplicationModule(displayName = "Ordering", allowedDependencies = {})
package com.acme.shop.ordering;
import org.springframework.modulith.ApplicationModule;
package com.acme.shop.ordering;
public class OrdersPage extends WebPage {
@SpringBean
private Orders orders;
public OrdersPage() {
var list = new WebMarkupContainer("list");
add(list.setOutputMarkupId(true));
var open = LoadableDetachableModel.of(orders::open);
list.add(new ListView<>("orders", open) {
@Override
protected void populateItem(ListItem<Order> item) {
Order order = item.getModelObject();
String id = order.id();
item.add(new Label("customer", order.customer()));
item.add(Oat.Components.button("ship", "Ship", ajax -> {
orders.ship(id);
Oat.toast(ajax, "Shipped", OatVariant.SUCCESS);
ajax.add(list);
}));
}
});
}
}
<!-- Plain HTML. A designer can open it in a browser. -->
<h1>Open orders</h1>
<ul wicket:id="list">
<li wicket:id="orders">
<span wicket:id="customer">Jane Doe</span>
<button wicket:id="ship">Ship</button>
</li>
</ul>
package com.acme.shop.ordering.internal;
@Service
@Transactional
public class Orders {
private final OrderRepository repository;
private final ApplicationEventPublisher events;
Orders(OrderRepository repository,
ApplicationEventPublisher events) {
this.repository = repository;
this.events = events;
}
public Order place(String customer, List<Order.Line> lines) {
return repository.save(Order.open(customer, lines));
}
public List<Order> open() {
return repository.findByShippedAtIsNull();
}
public void ship(String id) {
var order = repository.findById(id).orElseThrow().ship();
repository.save(order);
// billing and shipping react to this; ordering never calls them
events.publishEvent(new OrderShipped(order.id()));
}
}
// OrderShipped.java, in the base package: other slices
// may use it; everything in internal stays hidden
public record OrderShipped(String orderId) {}
package com.acme.shop.ordering.internal;
// Stored as one document, lines included: no join table,
// no mapping layer, no migration script for a new field.
@Document("orders")
public record Order(@Id String id, String customer,
List<Line> lines, Instant shippedAt) {
public record Line(String product, int quantity) {}
public static Order open(String customer, List<Line> lines) {
if (lines.isEmpty()) {
throw new IllegalArgumentException("an order needs lines");
}
return new Order(null, customer, List.copyOf(lines), null);
}
public Order ship() {
if (shippedAt != null) {
throw new IllegalStateException("already shipped");
}
return new Order(id, customer, lines, Instant.now());
}
}
// OrderRepository.java: the whole persistence layer
public interface OrderRepository
extends MongoRepository<Order, String> {
List<Order> findByShippedAtIsNull();
}
package com.acme.shop.ordering;
/// Each test starts with the id of the requirement it proves.
/// The requirements live in this slice's `package-info.java`.
@ApplicationModuleTest
// a real MongoDB in Docker; a replica set, so transactions work
@Import(MongoDbReplicaSetConfiguration.class)
class OrdersTests {
static final List<Order.Line> ONE_WIDGET =
List.of(new Order.Line("Widget", 1));
@Autowired Orders orders;
@Autowired OrderRepository repository;
@BeforeEach
void startEmpty() {
repository.deleteAll();
}
@Test
@DisplayName("R1.1 a placed order is listed as open")
void placedOrderIsOpen() {
var order = orders.place("ACME", ONE_WIDGET);
assertThat(orders.open())
.extracting(Order::id).containsExactly(order.id());
}
@Test
@DisplayName("R1.2 an order without lines is rejected")
void orderWithoutLinesIsRejected() {
assertThatThrownBy(() -> orders.place("ACME", List.of()))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("R2.1 a shipped order is no longer open")
void shippedOrderIsNotOpen() {
var order = orders.place("ACME", ONE_WIDGET);
orders.ship(order.id());
assertThat(orders.open()).isEmpty();
}
@Test
@DisplayName("R2.2 shipping an order publishes OrderShipped")
void shippingPublishesOrderShipped(Scenario scenario) {
var order = orders.place("ACME", ONE_WIDGET);
scenario.stimulate(() -> orders.ship(order.id()))
.andWaitForEventOfType(OrderShipped.class)
.toArriveAndVerify(event ->
assertThat(event.orderId()).isEqualTo(order.id()));
}
@Test
@DisplayName("R2.3 an order cannot ship twice")
void orderCannotShipTwice() {
var order = orders.place("ACME", ONE_WIDGET);
orders.ship(order.id());
assertThatThrownBy(() -> orders.ship(order.id()))
.isInstanceOf(IllegalStateException.class);
}
}
class ArchitectureTests {
ApplicationModules modules =
ApplicationModules.of(ShopApplication.class);
@Test
void slicesStayOutOfEachOthersInternals() {
// fails on cycles and on any use of another slice's
// internal package, from the root too
modules.verify();
}
@Test
void followsTheFullstack4jRules() {
// what verify() leaves alone: slice names, what a base package
// holds, documents, events, pages, and no @Disabled tests
Fullstack4jRules.builder(ShopApplication.class)
.build()
.checkAll();
}
@Test
void documentsTheArchitecture() {
new Documenter(modules).writeDocumentation();
}
}
/// Keeps every slice's spec and its tests in step.
///
/// A requirement id in a slice's `package-info.java` with no test
/// naming it in its `@DisplayName` is a gap. A test naming an id the
/// spec doesn't have is drift. Either one fails the build.
class SpecTraceabilityTests {
static final Path MAIN = Path.of("src/main/java");
static final Path TEST = Path.of("src/test/java");
static final Pattern REQUIREMENT = Pattern.compile(
"^\\s*///\\s*-\\s*(R\\d+\\.\\d+)\\b", Pattern.MULTILINE);
static final Pattern TESTED = Pattern.compile(
"@(?:[\\w.]+\\.)?DisplayName\\(\\s*\"(R\\d+\\.\\d+)\\b");
@TestFactory
Stream<DynamicTest> everySliceMatchesItsSpec() throws IOException {
try (var files = Files.walk(MAIN)) {
return files
.filter(file -> file.endsWith("package-info.java"))
.filter(spec -> !ids(REQUIREMENT, spec).isEmpty())
.map(spec -> MAIN.relativize(spec.getParent()))
.map(slice -> dynamicTest(name(slice), () -> {
var required = ids(REQUIREMENT, MAIN.resolve(slice)
.resolve("package-info.java"));
var tested = testedIds(TEST.resolve(slice));
assertAll(
() -> assertThat(without(required, tested))
.as("requirements without a test")
.isEmpty(),
() -> assertThat(without(tested, required))
.as("tests without a requirement")
.isEmpty());
}))
.toList()
.stream();
}
}
static Set<String> testedIds(Path dir) {
if (!Files.isDirectory(dir)) {
return Set.of();
}
try (var files = Files.walk(dir)) {
return files
.filter(file -> file.toString().endsWith(".java"))
.flatMap(file -> ids(TESTED, file).stream())
.collect(toCollection(TreeSet::new));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static Set<String> ids(Pattern pattern, Path file) {
try {
return pattern.matcher(Files.readString(file)).results()
.map(match -> match.group(1))
.collect(toCollection(TreeSet::new));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static Set<String> without(Set<String> these, Set<String> those) {
var rest = new TreeSet<>(these);
rest.removeAll(those);
return rest;
}
static String name(Path slice) {
return slice.toString()
.replace(slice.getFileSystem().getSeparator(), ".");
}
}
Built for working with agents
Boring technology is AI-friendly technology.
Coding agents are only as good as the feedback they get. A single typed language, fast tests,
architecture rules that fail loudly and a short spec in every slice give them exactly that, so a team
of three can review more than it has to type.
You: shape the next piece→Agent: specs it with you, then builds→Build: compiles, tests, checks boundaries and spec→You: review and ship
Guardrails, not guesswork
Modulith and ArchUnit tests catch an agent that wanders into the wrong module, or switches off a failing test, before a human has to.
One language in context
No TypeScript types drifting from Java DTOs. The agent sees the page, the service and the data in one place.
Feedback in seconds
The compiler, JUnit, Mockito and WicketTester check UI and logic without a browser, and Testcontainers gives every test run a real MongoDB.
The spec lives in the slice
Each slice carries its contract in its own package-info.java: operations, numbered requirements, events, out of scope. Every requirement has a test with its id, and the build fails when the two drift apart. One screen an agent can re-read on every loop. Based on SBCE by Adam Bien.
SBCE (spec-driven Boundary-Control-Entity) was created by Adam Bien and is described at
sbce.space. We use its spec: the package's own
doc, with numbered requirements that each have a test. Inside a slice we keep Spring Modulith's default
instead of boundary/control/entity: the base package is what other slices and the start page may use,
everything else goes in internal.
Beyond the code
The organisational dividend.
Fewer moving parts in the stack means fewer moving parts in the organisation. That is where most of the
savings come from, and they are easy to overlook.
Conway's law, on purpose
Teams build systems shaped like their communication. Shape the team around features and the system follows the product, not the tech.
No waiting on specialists
No queue for "the frontend person". Whoever is free takes the next most valuable slice, so work keeps flowing.
One hiring profile
Java and Spring developers are easy to find and slow to burn out on churn. You hire for one skill set and grow it.
Talk about users, not APIs
With little coordination to manage and an appetite instead of estimates, conversations turn to outcomes and value, not story points.
Onboarding in days
A new colleague reads one slice, ships a change to it, and already understands how every other slice works.
Calm dependencies
Mature, slow-moving libraries mean fewer forced migrations, so maintenance doesn't crowd out the roadmap.
For engineering leaders
A team of eight to twelve for a workflow-heavy business application, and it still feels slow.
The same scope with two or three people, one skill set and far fewer moving parts to run.
For founders and product people
Coordination eats the roadmap. Every idea needs a meeting before it needs code.
From idea to a shipped slice in days, with a team that talks about your users.
For Java developers
Stuck "on the backend", switching contexts, and tired of JavaScript churn.
Own features end to end in the language you know, with agents doing the boilerplate.
How the team works
Fixed time, variable scope: a rhythm built for small teams.
Our teams work with the core ideas of Shape Up, the method
Basecamp developed for small product teams. It fits the stack almost too well: its standard team is one
designer and two programmers, and it builds front end and back end together, one piece at a time. With
Wicket's plain HTML templates, the designer works in the same repository as everyone else.
Shaping the next bets, in parallel
1
2
3
4
5
6
7
8
Build · 6 weeksOne shaped project, the whole team, front end and back end together.
Cool-down · 2 weeksFixes, ideas, then the betting table.
Appetite, not estimates
Instead of estimating stories, decide how much time a problem is worth: two weeks or six. The date stays fixed and the scope flexes to fit it. No story points, no velocity charts.
Shaped before it's built
Experienced people shape the work first: the problem, a rough solution, the rabbit holes and what's out of scope. The pitch is the bet. Once the team takes it on, it becomes requirements in the slices it touches, and those stay when the cycle ends. Agents build fast; shaping and the spec make sure they build the right thing.
Whole scopes, one piece at a time
The team gets the whole problem, not a task list. Its scopes map naturally to slices and modules, and it gets one piece working end to end, page to database, first.
Bets, not backlogs
In each cool-down, decide which shaped pitches to bet on next. No backlog to groom. Work that doesn't ship within its cycle isn't extended by default; it gets reshaped.
Shape Up was written by Ryan Singer at Basecamp and is free to read at
basecamp.com/shapeup. We use its core ideas and adapt the
details to each team.
The toolbox
Small, open-source tools that take away the friction of day one.
The approach runs on standard Spring and Wicket. These helpers make the first hour pleasant and are
deliberately small, so they are easy to understand and to keep up to date.
start.fullstack4j.dev
fullstack4j start
Everything start.spring.io offers, plus Apache Wicket and the whole fullstack4j stack. Our link
preselects fullstack4j: Wicket, MongoDB, Spring Modulith and Testcontainers in two starters.
The whole stack in two dependencies: Wicket with Wicket Oat, Spring Modulith and MongoDB at versions
that work together, plus a test starter with a MongoDB replica set and the fullstack4j architecture
rules.
Apache Wicket 10 on Spring Boot 4 with a single dependency: auto-configured filter,
@SpringBean injection, DevTools support. Small enough to read in one sitting.
Page tests that survive refactoring: find components by type, wicket:id, label and
the row of a list instead of by path, and compare whole tables with an example. Failures say
what was searched and what came close.
Teaches your coding agent this way of working: it writes a slice's spec with you, then a test for
every requirement, then the code, until ./mvnw verify is green. Works with Claude Code
and other agents that load skills.
A cooperative of senior engineers, not a one-person show.
fullstack4j is an offering of allwerker eG, an IT cooperative
founded in Munich in 2023. Its members are independent senior engineers, architects and consultants
who run the cooperative together as equal partners. You work directly with the people who deliver.
19senior engineers, architects and consultants
Since 2023a registered cooperative in Munich
Solo or as a teamone expert or a whole team, as you need
Full-stack & Java engineers
Build slices end to end, from the Wicket page to the database, with the habits that keep them clean.
Software architects
Domain- and event-driven design, sensible module cuts, and replacing legacy systems step by step.
Platform & cloud engineers
Run the one deployable wherever it fits: a single server with Docker and Kamal, or Kubernetes on AWS, Google Cloud or Azure.
Security & compliance
Secure development, identity and access management, ISO 27001 and GDPR, built in from the first slice.
AI & data
AI platforms, machine learning and data engineering for when the product needs more than coding agents.
Operations & stability
Monitoring, incident response and performance work, so the product stays up long after launch.
No single point of failure
A team of three can't afford to stall when one person is out. The cooperative covers absences and brings in a specialist for a week when a slice needs one.
Organised the way we ask you to work
Equal partners, shared responsibility, no middle management. The cooperative runs on the same principles as the small teams it builds.
A partner beyond the project
allwerker aims for long-term partnerships, so the people who set up your team are still around when you need them again.
Your first contact
Daniel Bartl leads fullstack4j at allwerker.
Software engineer and agile practitioner in Munich. In 2004 he founded a Java and Spring consultancy and
grew it to more than a hundred people in self-managing teams without middle management, until it was
acquired in 2021. He has organised the Lightweight Java User Group
since 2010 and maintains the fullstack4j toolbox.
Tools are the easy part. What makes a small team work is a shared way of slicing the product, a few
firm conventions and the habit of shipping in small steps. allwerker builds that with your people,
and brings its own engineers whenever you need more hands.
1–2 days
Fit check
A workshop on your product and your team. We map the domain into slices with Event Modeling, shape
the first pitch, look at what you already run, and give you an honest go or no-go.
A slice map, a first shaped pitch with the spec for its first slice, and a clear decision.
Up to 6 weeks
First cycle
We set up the repository, modules, architecture tests, CI and hosting, plus the spec-and-test loop
your agents work in, and ship the first shaped project within its appetite, together with the people
who will own it.
A running product and a team that knows how to shape and ship the next piece.
Ongoing, part-time
Run & coach
We stay on as pairing and mob partners, or as the team itself, running the cycles with you:
shaping, betting, building, cool-down. You decide how far we step back.
A team that keeps its pace, with or without us.
Who builds it
The method stays the same. Who writes the code is up to you.
Your team, coached
Your developers build the product. allwerker sets up the skeleton, pairs and coaches, and steps back as your team takes over.
Mixed team
One or two allwerker engineers join your people, fill the gaps and pass the habits on by working side by side.
allwerker team
A complete team of three from the cooperative builds your product, then hands it over or keeps running it, as you prefer.
your developersallwerker engineers
In preparation
Trainings
Hands-on courses for teams that want to learn the approach on their own terms, as part of
allwerker's training and workshop offering. Get notified when the first dates are set.
Modular monoliths with Spring Modulith & ArchUnit 1 day
Spec-driven slices with coding agents 1 day
Honest fit
When it's the right call, and when it isn't.
No approach fits everything. Here is where this one shines and where we'd tell you to choose something else.
A great fit
Business applications, internal tools, B2B SaaS, back-office and portals
Products full of forms, tables, workflows and rules
Teams with Java skills, or a JVM landscape they want to build on
Legacy systems you want to replace slice by slice
Probably not
Offline-first apps or real-time collaborative editors
Consumer apps that need to feel native on every device
Content and marketing sites, where a static site generator is simpler
Teams with no JVM experience and no wish to build it
FAQ
Questions we hear a lot.
Who will actually work with us?
Daniel Bartl leads fullstack4j and is your first contact. The engineers come from allwerker, chosen for
what your product needs: full-stack and Java engineers for the slices, platform and cloud engineers to
run them, and security or data specialists when a slice calls for it. You work with them directly.
Do you work in sprints?
We work in Shape Up-style cycles: shaped work, a fixed time box and a cool-down in between. If your
organisation runs Scrum or Kanban, we adapt. The slice structure works with any of them.
Isn't Apache Wicket old technology?
It is mature: an Apache project with two decades of production use. Wicket 10 runs on Jakarta EE 10, the
current Spring Boot and Java 25, which is what we build on. "Old" mostly means it has kept working while other frameworks were
rewritten. That is exactly what you want under a product you plan to keep for ten years.
Can a server-rendered UI feel modern?
Yes. Wicket updates parts of a page via Ajax without you writing JavaScript, and Wicket Oat gives you
modern, themeable, accessible components. Where a page really needs a rich client widget, such as a
chart, a map or an editor, you embed it into that page and leave the rest alone.
Why MongoDB and not a relational database?
A slice's data usually has one natural shape, like an order with its lines. MongoDB stores it as one
document, exactly as the code models it, so the schema grows with the slice instead of through
migration scripts. Testcontainers gives every developer and every CI run a real MongoDB, with
nothing to install. If your domain is strongly relational or the database is already decided,
Spring Data JPA fits the same slice structure.
Can slices call each other directly?
Yes, when an event doesn't fit, for example when a page has to show another slice's data right away.
Events stay the first choice because the slice that publishes them doesn't need to know who listens.
A slice that has to answer directly puts an interface and the records it returns in its base package.
The implementation stays in internal, next to the MongoDB documents, so both can change
without breaking anyone. Spring Modulith fails the build when anything else in the slice is used.
What happens when we grow beyond three people?
Modules become team boundaries. Add a second small team that owns a group of slices, not a layer. Spring
Modulith keeps the boundaries explicit, so if a module ever really needs to become its own service, the
cut is already there.
We already run a SPA and microservices. Do we start over?
No. New slices go into a modular application next to what you have, and old parts are replaced one at
a time when there's a reason to touch them. Incremental modernisation of legacy systems is part of what
we do.
Why Wicket and not Vaadin or HTMX?
Both are good choices with the same philosophy, and we're happy to talk about them. We prefer Wicket
for its plain HTML templates, its strong component model and how easy it is to unit-test UI with
WicketTester.
How much lock-in is there?
Very little. Everything is Apache-licensed open source, and the fullstack4j helpers are thin on purpose.
You can drop them at any time and keep plain Wicket and plain Spring Boot.
Do we have to use AI agents?
No. The approach stands on its own and was useful long before coding agents. Agents make a small team
even more effective, and the architecture tests make them safe to use. How far you go is up to you.
The spec in each slice pays off without them too: it is the first thing a new colleague reads.
Isn't a spec just more documentation to keep up?
Not this kind. It lives in the slice's own package-info.java, so it moves and refactors with
the code, and every requirement has a test that carries its id. When spec and tests disagree, the build
fails. If you need Markdown for a portal or for agents, generate it from the code with
jmarkdoc instead of
writing a second copy.
Let's talk
Could your next product be built by a team of three?
Tell us what you're building and who's building it. We'll tell you honestly whether this approach fits, where we'd start, and who from allwerker should be involved.