Core stack

Java 25

Latest language features: records for every DTO, pattern matching, virtual-thread ready.

Language

Spring Boot 4.1

Web MVC, validation, caching and security wired together with sensible defaults.

Framework

MongoDB

Document store via Spring Data. Posts carry no author reference of any kind.

Database

Thymeleaf

Server-rendered pages with shared layout fragments, plus Spring Security tags for nav state.

Templating

Spring Security

Admin-only login for moderation. Everyone else posts and reads without an account.

Security

Jakarta Validation

Bean Validation on request records, surfaced as a consistent JSON error body.

Validation

Spring Web MVC

Page controllers for Thymeleaf views and REST controllers for the JSON API, with one shared error handler.

Web layer

Spring Data MongoDB

Repository interfaces over the document model, so queries stay short and typed.

Data access

Spring Cache

In-process cache on the public feed, evicted whenever a write could change what people see.

Performance

Frontend

Vanilla JavaScript

No framework. Small scripts use fetch to call /api/** for posting, reacting and moderation.

Interactivity

Custom CSS

A hand-written theme with design tokens, glassy nav and soft cards. No Tailwind or Bootstrap.

Styling

Google Fonts

Inter for the interface and Kalam for the handwritten feel of each note.

Typography

How it fits together

BrowserThymeleaf pages + vanilla JS
โ†’
REST API/api/** controllers
โ†’
ServicesRate limiters + cache
โ†’
MongoDBPosts & admin users

Design choices we like

Anonymous by design

No userId, hash or session on a post, so nothing can trace it back, even with database access.

In-memory rate limits

Per-IP guards for posting, relating, flagging and views. Nothing persisted, no durable trail.

Cached feed

The public feed is cached in-process and evicted on every write that could change it.

Tested layers

Mockito unit tests, a minimal Spring context test for cache AOP, and validator boundary tests.

Patterns & concepts

Layered architecture

Controller โ†’ Service โ†’ Repository. Each layer has one job and only talks to the one below it.

MVC

Thymeleaf views, page controllers and model data kept apart, with a separate REST layer for the JSON API.

Repository pattern

PostRepository and UserRepository hide all Mongo access behind plain interfaces.

DTO pattern

Java records like PostResponse and PostCreateRequest mean documents never reach the web layer.

Dependency injection

Constructor injection through Spring, with Lombok's @RequiredArgsConstructor keeping it tidy.

Proxy & AOP caching

@Cacheable and @CacheEvict run on a Spring proxy: cache-aside for reads, eviction on writes.

Builder pattern

User is created with Lombok's @Builder and @Builder.Default values.

Centralised error handling

Custom exceptions plus one @RestControllerAdvice give every API error the same JSON shape.

Scheduled task

A @Scheduled job refreshes the feed cache every minute so view counts never look stuck.

Immutability & enums

Records for data carriers, enums like PostStatus and Role for typed states instead of strings.

Thread-safe shared state

Rate limiters use ConcurrentHashMap sets and deques so parallel requests stay correct.

Secure password storage

Admin passwords are hashed with BCrypt, and UserService plugs into Spring Security as a UserDetailsService.

Hosting & delivery

Render

Hosts the app as a Docker container, deployed straight from git via a Render Blueprint.

Hosting

MongoDB Atlas

Cloud-hosted MongoDB with two separate databases, one for development and one for production.

Cloud database

Cloudflare

Sits in front of the site for DNS and fast, secure delivery to visitors.

DNS & Edge

.tech domain

Home at seughost.tech, a domain that fits a project built for a tech campus.

Domain

Tooling & testing

Maven Lombok Spring DevTools JUnit 5 Mockito Spring Test Spring Security Test

Advanced Java in this project

Streams API & method references

PostService.getPublicFeed()
return postRepository
        .findByStatusOrderByPinnedDescCreatedAtDesc(PostStatus.APPROVED)
        .stream()
        .map(this::toPostResponse)
        .toList();

Records & Bean Validation

PostCreateRequest
public record PostCreateRequest(
        @NotNull(message = "A post type is required.")
        PostType type,

        @NotBlank(message = "Thoughts can't be empty.")
        @Size(min = 3, max = 1000)
        String content,
        // ...
) { }

Generics & Optional

UserRepository
public interface UserRepository
        extends MongoRepository<User, String> {

    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
}

Collections framework

PostService.normalizeTags()
Set<String> deduped = new LinkedHashSet<>();
for (String tag : tags) {
    String trimmed = tag.trim();
    if (!trimmed.isEmpty()) {
        deduped.add(trimmed);
    }
    if (deduped.size() == MAX_TAGS) {
        break;
    }
}
return new ArrayList<>(deduped);

Concurrency & java.time

PostCreationRateLimiter
private final Map<String, ConcurrentLinkedDeque<Long>> postTimesByIp
        = new ConcurrentHashMap<>();

long now = Instant.now().toEpochMilli();
ConcurrentLinkedDeque<Long> timestamps =
        postTimesByIp.computeIfAbsent(ip,
                key -> new ConcurrentLinkedDeque<>());

synchronized (timestamps) {
    // prune old entries, check cooldown, record this post
}

Servlet API

PostApiController.clientIp()
private String clientIp(HttpServletRequest request) {
    String forwardedFor = request.getHeader("X-Forwarded-For");
    if (forwardedFor != null && !forwardedFor.isBlank()) {
        return forwardedFor.split(",")[0].trim();
    }
    return request.getRemoteAddr();
}

Custom exceptions

AlreadyRelatedException
public class AlreadyRelatedException
        extends RuntimeException {

    public AlreadyRelatedException(String message) {
        super(message);
    }
}

Enums

PostStatus &amp; nested Result
public enum PostStatus {
    PENDING,
    APPROVED,
    REJECTED
}

// inside PostCreationRateLimiter
public enum Result { OK, COOLDOWN, DAILY_CAP }

Annotations, Lombok & DI

HomeController
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final PostService postService;

    @GetMapping("/")
    public String feed(Model model) { ... }
}

Unit testing with Mockito

PostServiceTest
@ExtendWith(MockitoExtension.class)
class PostServiceTest {
    @Mock private PostRepository postRepository;
    @InjectMocks private PostService postService;

    @Test
    void createPost_nullType_throwsInvalidPostException() {
        var request = new PostCreateRequest(null, null,
                "Some thoughts.", null, List.of());

        assertThatThrownBy(() -> postService.createPost(request))
                .isInstanceOf(InvalidPostException.class);
    }
}