0% прочитано

When a New Feature Does Not Need a New Table

A new feature does not always require a new table. If the system already has thread and message models, a public discussion may be another bounded use case of the same persistence model. But shared storage must not blur access rules, domain events, or behavior across different parts of the system.

26 жовтня 2026 р. 9 хв читанняLaravel

When a new feature appears, it is easy to jump straight to a new migration: new concept, new table.

For a public discussion, the first idea may be a new comments table.

But if the system already has threads and messages, the better first question is: did a new data model actually appear, or is this another use case of an existing one?

Reuse an existing model not to save one migration, but when the new feature truly shares the same entity, lifecycle, and persistence rules.

Start With the Meaning of the Data, Not the Schema

Two features may have identical columns and still represent different models. Conversely, two different interfaces may store the same underlying entity.

So I compare meaning before columns:

  • what the core entity actually is;
  • who creates it;
  • how it changes over time;
  • what deletion means;
  • how child records are ordered;
  • which integrity rules must hold.

An Existing Thread and Message Model May Already Fit

Generalized existing model
Thread  id  context_type  context_id  ... Message  id  thread_id  author_id  body  created_at  ...

If the existing model already represents an ordered sequence of messages inside a context, a public discussion may simply be another context for that same structure.

In that case, a new comments table may duplicate what the system already stores: author, body, creation time, message order, and association with a parent entity.

But this is correct only when the meaning matches, not merely the shape of the data.

Shared Persistence Does Not Mean Shared Behavior

The most important boundary is between persistence and behavior.

The same tables may support several use cases while their access rules remain very different.

A public discussion may be readable by everyone, while a private support thread may be visible only to its participants.

Shared persistence is not permission to merge those rules into one vague access check.

Keep Access Rules Separate

One model does not require one access policy
final class PublicDiscussionPolicy{    public function view(User $user, Thread $thread): bool    {        // Rules for public discussions.    }} final class PrivateConversationPolicy{    public function view(User $user, Thread $thread): bool    {        // Rules for private conversations.    }}

Even if both use cases work with Thread, they do not have to share one policy, one service, or one set of checks.

Separate rules make the boundary explicit: persistence is reused, access decisions are not blurred together.

Do Not Merge Domain Events Just Because the Table Is Shared

Different events may rely on the same message model
PublicDiscussionMessageCreated PrivateConversationMessageCreated

Creating a message in a public discussion may have very different consequences from creating one in a private conversation.

One action may trigger public notifications while the other triggers private ones. One may be moderated while the other is not.

So a shared table does not mean the whole system should react to one context-free MessageCreated event.

I discuss event choice in more detail in the separate article about when an event is the right choice.

Reuse Has a Boundary Too

Not every new feature should be forced into an existing table at any cost.

A warning sign appears when the new use case requires many fields that make no sense for existing use cases, or nearly every query begins with a large set of type-specific conditions.

At that point, the shared table is no longer simplifying the system. It is hiding two different models behind one name.

When a shared table starts losing one coherent meaning
threads  support_ticket_id nullable  post_id nullable  order_id nullable  moderation_state nullable  private_recipient_id nullable  public_sort_weight nullable  ...

A large number of mutually exclusive nullable fields often means the model is serving concepts that have drifted too far apart.

This is not automatic proof that separate tables are required, but it is a strong reason to reassess the model boundary.

Avoiding a Migration Is Not the Goal

A new table is not a problem. If the feature truly introduces a separate entity with its own rules, create the table.

The problem is creating a duplicate of an existing model only because the new feature has a different label in the interface.

Over time, the same concerns then need to be solved twice: pagination, message order, deletion, authorship, search, moderation, and data integrity.

Questions to Ask Before Adding a Table

A short check before adding a migration
1. Is this truly a new entity?2. Does the data lifecycle match?3. Do authorship, ordering, and deletion behave the same way?4. Can access rules stay separate without duplicating storage?5. Can domain events remain use-case specific?6. Are many fields needed by only one use case?7. Are queries becoming constant type checks?8. Which model will be easier to maintain a year from now?

Test the Boundaries When Reusing a Model

When one model serves several use cases, the most important tests are not about inserting a row. They are about the boundaries between those use cases.

  • a public discussion must not expose a private conversation;
  • rules from one use case must not affect another;
  • the correct domain events must be emitted;
  • deletion or moderation must apply only where intended;
  • queries must return only records from the required context.

A New Feature Does Not Always Mean a New Data Model

If the system already has thread and message models, a new public discussion may be another use case of the same model.

That avoids duplicating persistence and technical problems the system has already solved.

But reuse works only when the boundaries remain explicit: access rules, domain events, and behavior from each use case must not bleed into one another.

The question is not whether you can avoid a new table. The question is whether the new feature truly represents the same data used in a different way.

Reuse the data model, not the rules of unrelated use cases.